From 198c8c91cc59fd5f68c35b7c73b5666b679c114c Mon Sep 17 00:00:00 2001 From: Justin Burke Date: Wed, 3 Aug 2016 08:30:42 -0700 Subject: [PATCH 001/769] Add user flag. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d7c6882..b93216e2 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ declare -x AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXxx" When the deploy proccess is done a new server will be placed in the local inventory file `inventory_users`. If you want to add or delete users, update the `users` list in `config.cfg` and run the playbook `users.yml`. This command will update users on any servers in the file `inventory_users`. ``` -ansible-playbook users.yml -i inventory_users +ansible-playbook users.yml --user=root -i inventory_users ``` Note: For EC2 users, Algo does NOT use EC2 dynamic inventory for user management. Please continue to use users.yml playbook as described below. This may be subject to change in the future. From 1e4d3ab32aa54b8af745cfb5be72ff3e5f3d25c6 Mon Sep 17 00:00:00 2001 From: Defunct Date: Wed, 3 Aug 2016 20:03:46 +0000 Subject: [PATCH 002/769] fix EC2 security group rules --- ec2.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ec2.yml b/ec2.yml index 200046f4..486f9913 100644 --- a/ec2.yml +++ b/ec2.yml @@ -74,9 +74,13 @@ description: Security group for VPN servers region: "{{ regions[region] }}" rules: - - proto: tcp - from_port: 443 - to_port: 443 + - proto: udp + from_port: 4500 + to_port: 4500 + cidr_ip: 0.0.0.0/0 + - proto: udp + from_port: 500 + to_port: 500 cidr_ip: 0.0.0.0/0 - proto: tcp from_port: 22 From a15939a7c69c0adb241a8e709bc245039195fada Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Aug 2016 20:31:34 +0300 Subject: [PATCH 003/769] AppArmor policy for Privoxy #40 --- features.yml | 19 ++++++++++++++++--- templates/usr.sbin.privoxy.j2 | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 templates/usr.sbin.privoxy.j2 diff --git a/features.yml b/features.yml index 4ac75807..03e4afe6 100644 --- a/features.yml +++ b/features.yml @@ -27,6 +27,14 @@ notify: - restart privoxy + - name: Privoxy profile for apparmor configured + template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=600 + notify: + - restart privoxy + + - name: Enforce the privoxy AppArmor policy + shell: aa-enforce usr.sbin.privoxy + - name: Privoxy enabled and started service: name=privoxy state=started enabled=yes @@ -36,15 +44,17 @@ apt: name=dnsmasq state=latest - name: Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq + template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=600 + notify: + - restart dnsmasq - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq - notify: - - restart apparmor - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf + notify: + - restart dnsmasq - name: Adblock script created copy: src=templates/adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 @@ -95,6 +105,9 @@ - name: restart privoxy service: name=privoxy state=restarted + - name: restart dnsmasq + service: name=dnsmasq state=restarted + - name: restart apparmor service: name=apparmor state=restarted diff --git a/templates/usr.sbin.privoxy.j2 b/templates/usr.sbin.privoxy.j2 new file mode 100644 index 00000000..5f8d9ddf --- /dev/null +++ b/templates/usr.sbin.privoxy.j2 @@ -0,0 +1,15 @@ +#include + +/usr/sbin/privoxy { + #include + #include + + capability setgid, + capability setuid, + + /etc/privoxy/* r, + /etc/privoxy/templates/* r, + /run/privoxy.pid w, + /var/log/privoxy/logfile w, + +} From a1a0d041b18eb2a972221ad9817fed454090a746 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Aug 2016 21:05:07 +0300 Subject: [PATCH 004/769] User friendly provider UI #28 --- README.md | 22 +++++++++++++--------- deploy.yml | 9 --------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b93216e2..5eaacfc7 100644 --- a/README.md +++ b/README.md @@ -30,23 +30,27 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * python >= 2.6 * libselinux-python (for RedHat based distros) * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) +* SHell or BASH ### Initial Deployment -Open the file `config.cfg` in your favorite text editor. Change `server_name` and specify users in the `users` list. Start the deploy and follow the instructions (available options for PROVIDER are `digitalocean` or `ec2`): - -``` -ansible-playbook deploy.yml -e "provider=PROVIDER" -``` - -When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext since it contains the keys to access the VPN. For those using other clients, like Windows or Android, send the X.509 certificates for the server and their user. - -Note: For EC2 users, ensure that you setup the required environment variables prior to starting the deploy: +**Available cloud providers:** +* DigitalOcean +* Amazon EC2 +Note: For EC2 users, ensure that you setup the required environment variables prior to starting the deploy: ``` declare -x AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXXXXX" declare -x AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXxx" ``` +Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. Start the deploy and follow the instructions: + +``` +./run +``` + +When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext since it contains the keys to access the VPN. For those using other clients, like Windows or Android, send the X.509 certificates for the server and their user. + ### User Management diff --git a/deploy.yml b/deploy.yml index f1ca5c46..9e06c583 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,15 +1,6 @@ --- -- hosts: localhost - gather_facts: false - tasks: - - fail: - msg: - - 'You need to define `provider` variable. Read README.md for more details' - when: provider is not defined - - include: "{{ provider }}.yml" - when: provider is defined - include: common.yml - include: security.yml - include: features.yml From 988f72b4280b3895011a0da175bf1f097f77ebc5 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Aug 2016 21:05:39 +0300 Subject: [PATCH 005/769] User friendly provider UI #28 --- run | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100755 run diff --git a/run b/run new file mode 100755 index 00000000..2b108bb8 --- /dev/null +++ b/run @@ -0,0 +1,18 @@ +#!/bin/sh + +echo -n " +What provider would you like to use? + 1. DigitalOcean + 3. Amazon EC2 +Enter the number of your desired provider +: " + +read N + +case "$N" in + 1) CLOUD="digitalocean" ;; + 2) CLOUD="ec2" ;; + *) exit 1 ;; +esac + +ansible-playbook deploy.yml -e "provider=${CLOUD}" From 2b9dde6016a94b352340768288caca3916ba994b Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Aug 2016 22:58:29 +0300 Subject: [PATCH 006/769] mod_pagespeed #5 --- config.cfg | 15 +- digitalocean.yml | 5 - features.yml | 64 +++++- inventory_users | 1 + templates/000-default.conf.j2 | 11 + templates/pagespeed.conf.j2 | 369 ++++++++++++++++++++++++++++++++++ templates/ports.conf.j2 | 13 ++ templates/privoxy_config.j2 | 2 + 8 files changed, 459 insertions(+), 21 deletions(-) create mode 100644 templates/000-default.conf.j2 create mode 100644 templates/pagespeed.conf.j2 create mode 100644 templates/ports.conf.j2 diff --git a/config.cfg b/config.cfg index 6fd84bf8..bfd3aa2f 100644 --- a/config.cfg +++ b/config.cfg @@ -5,17 +5,22 @@ easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 easyrsa_p12_export_password: vpn -# if True re-init all existing certificates. -easyrsa_reinit_existent: True +# If True re-init all existing certificates. (True or False) +easyrsa_reinit_existent: False +vpn_network: 10.19.48.0/24 +vpn_network_ipv6: 'fd9d:bc11:4021:69ce::/64' +server_name: "{{ ansible_ssh_host }}" + +# Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) +service_dns: True + +# If you don't want to use a local DNS resolver (option `service_dns`) you need to define DNS servers in this list. dns_servers: - 8.8.8.8 - 8.8.4.4 - 2001:4860:4860::8888 - 2001:4860:4860::8844 -vpn_network: 10.19.48.0/24 -vpn_network_ipv6: 'fd9d:bc11:4021:69ce::/64' -server_name: "{{ ansible_ssh_host }}" users: - mr.smith diff --git a/digitalocean.yml b/digitalocean.yml index a4358836..d46c175a 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -46,11 +46,6 @@ prompt: "Name the vpn server:\n" default: "algo.local" private: no - - - name: "service_dns" - prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N)" - default: "Y" - private: no tasks: - name: "Getting your SSH key ID on Digital Ocean..." diff --git a/features.yml b/features.yml index 03e4afe6..12d7625a 100644 --- a/features.yml +++ b/features.yml @@ -2,7 +2,6 @@ - name: Other features hosts: vpn-host - gather_facts: false become: true vars_files: - config.cfg @@ -17,7 +16,7 @@ - name: Loopback is running shell: ifdown lo:100 && ifup lo:100 - # Privoxy + #Privoxy - name: Install privoxy apt: name=privoxy state=latest @@ -38,6 +37,46 @@ - name: Privoxy enabled and started service: name=privoxy state=started enabled=yes + # PageSpeed + + - name: Apache installed + apt: name=apache2 state=latest + + - name: PageSpeed installed for x86_64 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb + when: ansible_architecture == "x86_64" + + - name: PageSpeed installed for i386 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb + when: ansible_architecture != "x86_64" + + - name: PageSpeed configured + template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf + notify: + - restart apache2 + + - name: Modules enabled + apache2_module: state=present name="{{ item }}" + with_items: + - proxy_http + - pagespeed + - cache + - proxy_connect + - proxy_html + - rewrite + notify: + - restart apache2 + + - name: VirtualHost configured for the PageSpeed module + template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf + notify: + - restart apache2 + + - name: Apache ports configured + template: src=ports.conf.j2 dest=/etc/apache2/ports.conf + notify: + - restart apache2 + # DNS - name: Install dnsmasq @@ -58,13 +97,16 @@ - name: Adblock script created copy: src=templates/adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 + when: service_dns is defined and service_dns == "True" - name: Adblock script added to cron cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" + when: service_dns is defined and service_dns == "True" - name: Update adblock hosts shell: > /opt/adblock.sh + when: service_dns is defined and service_dns == "True" - name: Forward all DNS requests to the local resolver iptables: @@ -77,7 +119,7 @@ to_destination: 172.16.0.1:53 notify: - save iptables - when: service_dns is defined and service_dns == "Y" # TODO: service_dns is not defined, because the variable in vars_prompt + when: service_dns is defined and service_dns == "True" - name: Forward all DNS requests to the local resolver iptables: @@ -91,15 +133,15 @@ ip_version: ipv6 notify: - save iptables - when: service_dns is defined and service_dns == "Y" + when: service_dns is defined and service_dns == "True" - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes - when: service_dns is defined and service_dns == "Y" + when: service_dns is defined and service_dns == "True" - name: Dnsmasq disabled and stopped service: name=dnsmasq state=stopped enabled=no - when: service_dns is defined and service_dns == "N" + when: service_dns is defined and service_dns == "False" handlers: - name: restart privoxy @@ -109,10 +151,10 @@ service: name=dnsmasq state=restarted - name: restart apparmor - service: name=apparmor state=restarted + service: name=apparmor state=restarted + + - name: restart apache2 + service: name=apache2 state=restarted - name: save iptables - command: service netfilter-persistent save - - - + command: service netfilter-persistent save diff --git a/inventory_users b/inventory_users index cafed486..8e9e7af6 100644 --- a/inventory_users +++ b/inventory_users @@ -1 +1,2 @@ [user-management] +37.139.0.99 diff --git a/templates/000-default.conf.j2 b/templates/000-default.conf.j2 new file mode 100644 index 00000000..7aa917b7 --- /dev/null +++ b/templates/000-default.conf.j2 @@ -0,0 +1,11 @@ + + + Order deny,allow + Allow from all + + RewriteEngine On + RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] + ProxyPass / http://$1 + ProxyPassReverse / http://$1 + ProxyPreserveHost On + diff --git a/templates/pagespeed.conf.j2 b/templates/pagespeed.conf.j2 new file mode 100644 index 00000000..3b89b758 --- /dev/null +++ b/templates/pagespeed.conf.j2 @@ -0,0 +1,369 @@ + + # Turn on mod_pagespeed. To completely disable mod_pagespeed, you + # can set this to "off". + ModPagespeed on + + # We want VHosts to inherit global configuration. + # If this is not included, they'll be independent (except for inherently + # global options), at least for backwards compatibility. + ModPagespeedInheritVHostConfig on + + # Direct Apache to send all HTML output to the mod_pagespeed + # output handler. + AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html + + # If you want mod_pagespeed process XHTML as well, please uncomment this + # line. + # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml + + # The ModPagespeedFileCachePath directory must exist and be writable + # by the apache user (as specified by the User directive). + ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" + + # LogDir is needed to store various logs, including the statistics log + # required for the console. + ModPagespeedLogDir "/var/log/pagespeed" + + # The locations of SSL Certificates is distribution-dependent. + ModPagespeedSslCertDirectory "/etc/ssl/certs" + + + # If you want, you can use one or more memcached servers as the store for + # the mod_pagespeed cache. + # ModPagespeedMemcachedServers localhost:11211 + + # A portion of the cache can be kept in memory only, to reduce load on disk + # (or memcached) from many small files. + # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 + + # Override the mod_pagespeed 'rewrite level'. The default level + # "CoreFilters" uses a set of rewrite filters that are generally + # safe for most web pages. Most sites should not need to change + # this value and can instead fine-tune the configuration using the + # ModPagespeedDisableFilters and ModPagespeedEnableFilters + # directives, below. Valid values for ModPagespeedRewriteLevel are + # PassThrough, CoreFilters and TestingCoreFilters. + # + ModPagespeedRewriteLevel CoreFilters + + ModPagespeedEnableFilters combine_heads + ModPagespeedEnableFilters combine_javascript + ModPagespeedEnableFilters convert_jpeg_to_webp + ModPagespeedEnableFilters convert_png_to_jpeg + ModPagespeedEnableFilters inline_preview_images + ModPagespeedEnableFilters make_google_analytics_async + ModPagespeedEnableFilters move_css_above_scripts + ModPagespeedEnableFilters move_css_to_head + ModPagespeedEnableFilters resize_mobile_images + ModPagespeedEnableFilters sprite_images + + ModPagespeedEnableFilters defer_iframe + ModPagespeedEnableFilters defer_javascript + ModPagespeedEnableFilters lazyload_images + + # Explicitly disables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, if one + # of the filters in the CoreFilters needs to be disabled for a + # site, that filter can be added to + # ModPagespeedDisableFilters. This directive contains a + # comma-separated list of filter names, and can be repeated. + # + # ModPagespeedDisableFilters rewrite_images + + # Explicitly enables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, filters + # not included in the CoreFilters may be enabled using this + # directive. This directive contains a comma-separated list of + # filter names, and can be repeated. + # + # ModPagespeedEnableFilters rewrite_javascript,rewrite_css + # ModPagespeedEnableFilters collapse_whitespace,elide_attributes + + # Explicitly forbids the enabling of specific filters using either query + # parameters or request headers. This is useful, for example, when we do + # not want the filter to run for performance or security reasons. This + # directive contains a comma-separated list of filter names, and can be + # repeated. + # + # ModPagespeedForbidFilters rewrite_images + + # How long mod_pagespeed will wait to return an optimized resource + # (per flush window) on first request before giving up and returning the + # original (unoptimized) resource. After this deadline is exceeded the + # original resource is returned and the optimization is pushed to the + # background to be completed for future requests. Increasing this value will + # increase page latency, but might reduce load time (for instance on a + # bandwidth-constrained link where it's worth waiting for image + # compression to complete). If the value is less than or equal to zero + # mod_pagespeed will wait indefinitely for the rewrite to complete before + # returning. + # + # ModPagespeedRewriteDeadlinePerFlushMs 10 + + # ModPagespeedDomain + # authorizes rewriting of JS, CSS, and Image files found in this + # domain. By default only resources with the same origin as the + # HTML file are rewritten. For example: + # + ModPagespeedDomain * + # + # This will allow resources found on http://cdn.myhost.com to be + # rewritten in addition to those in the same domain as the HTML. + # + # Other domain-related directives (like ModPagespeedMapRewriteDomain + # and ModPagespeedMapOriginDomain) can also authorize domains. + # + # Wildcards (* and ?) are allowed in the domain specification. Be + # careful when using them as if you rewrite domains that do not + # send you traffic, then the site receiving the traffic will not + # know how to serve the rewritten content. + + # If you use downstream caches such as varnish or proxy_cache for caching + # HTML, you can configure pagespeed to work with these caches correctly + # using the following directives. Note that the values for + # ModPagespeedDownstreamCachePurgeLocationPrefix and + # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here + # in order to force the webmaster to choose appropriate value for these. + # + # ModPagespeedDownstreamCachePurgeLocationPrefix + # ModPagespeedDownstreamCachePurgeMethod PURGE + # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 + # ModPagespeedDownstreamCacheRebeaconingKey + + # Other defaults (cache sizes and thresholds): + # + # ModPagespeedFileCacheSizeKb 102400 + # ModPagespeedFileCacheCleanIntervalMs 3600000 + # ModPagespeedLRUCacheKbPerProcess 1024 + # ModPagespeedLRUCacheByteLimit 16384 + # ModPagespeedCssFlattenMaxBytes 102400 + # ModPagespeedCssInlineMaxBytes 2048 + # ModPagespeedCssImageInlineMaxBytes 0 + # ModPagespeedImageInlineMaxBytes 3072 + # ModPagespeedJsInlineMaxBytes 2048 + # ModPagespeedCssOutlineMinBytes 3000 + # ModPagespeedJsOutlineMinBytes 3000 + # ModPagespeedMaxCombinedCssBytes -1 + # ModPagespeedMaxCombinedJsBytes 92160 + + # Limit the number of inodes in the file cache. Set to 0 for no limit. + # The default value if this paramater is not specified is 0 (no limit). + ModPagespeedFileCacheInodeLimit 500000 + + # Bound the number of images that can be rewritten at any one time; this + # avoids overloading the CPU. Set this to 0 to remove the bound. + # + # ModPagespeedImageMaxRewritesAtOnce 8 + + # You can also customize the number of threads per Apache process + # mod_pagespeed will use to do resource optimization. Plain + # "rewrite threads" are used to do short, latency-sensitive work, + # while "expensive rewrite threads" are used for actual optimization + # work that's more computationally expensive. If you live these unset, + # or use values <= 0 the defaults will be used, which is 1 for both + # values when using non-threaded MPMs (e.g. prefork) and 4 for both + # on threaded MPMs (e.g. worker and event). These settings can only + # be changed globally, and not per virtual host. + # + # ModPagespeedNumRewriteThreads 4 + # ModPagespeedNumExpensiveRewriteThreads 4 + + # Randomly drop rewrites (*) to increase the chance of optimizing + # frequently fetched resources and decrease the chance of optimizing + # infrequently fetched resources. This can reduce CPU load. The default + # value of this parameter is 0 (no drops). 90 means that a resourced + # fetched once has a 10% probability of being optimized while a resource + # that is fetched 50 times has a 99.65% probability of being optimized. + # + # (*) Currently only CSS files and images are randomly dropped. Images + # within CSS files are not randomly dropped. + # + # ModPagespeedRewriteRandomDropPercentage 90 + + # Many filters modify the URLs of resources in HTML files. This is typically + # harmless but pages whose Javascript expects to read or modify the original + # URLs may break. The following parameters prevent filters from modifying + # URLs of their respective types. + # + # ModPagespeedJsPreserveURLs on + # ModPagespeedImagePreserveURLs on + # ModPagespeedCssPreserveURLs on + + # When PreserveURLs is on, it is still possible to enable browser-specific + # optimizations (for example, webp images can be served to browsers that + # will accept them). They'll be served with Vary: Accept or Vary: + # User-Agent headers as appropriate. Note that this may require configuring + # reverse proxy caches such as varnish to handle these headers properly. + # + # ModPagespeedFilters in_place_optimize_for_browser + + # Internet Explorer has difficulty caching resources with Vary: headers. + # They will either be uncached (older IE) or require revalidation. See: + # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx + # As a result we serve them as Cache-Control: private instead by default. + # If you are using a reverse proxy or CDN configured to cache content with + # the Vary: Accept header you should turn this setting off. + # + # ModPagespeedPrivateNotVaryForIE on + + # Settings for image optimization: + # + # Lossy image recompression quality (0 to 100, -1 just strips metadata): + # ModPagespeedImageRecompressionQuality 85 + # + # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedJpegRecompressionQuality -1 + # ModPagespeedJpegRecompressionQualityForSmallScreens 70 + + ModPagespeedJpegRecompressionQuality 75 + + # + # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedWebpRecompressionQuality 80 + # ModPagespeedWebpRecompressionQualityForSmallScreens 70 + # + # Timeout for conversions to WebP format, in + # milliseconds. Negative values mean no timeout is applied. The + # default value is -1: + # ModPagespeedWebpTimeoutMs 5000 + # + # Percent of original image size below which optimized images are retained: + # ModPagespeedImageLimitOptimizedPercent 100 + # + # Percent of original image area below which image resizing will be + # attempted: + # ModPagespeedImageLimitResizeAreaPercent 100 + + # Settings for inline preview images + # + # Setting this to n restricts preview images to the first n images found on + # the page. The default of -1 means preview images can appear anywhere on + # the page (if those images appear above the fold). + # ModPagespeedMaxInlinedPreviewImagesIndex -1 + + # Sets the minimum size in bytes of any image for which a low quality image + # is generated. + # ModPagespeedMinImageSizeLowResolutionBytes 3072 + + # The maximum URL size is generally limited to about 2k characters + # due to IE: See http://support.microsoft.com/kb/208427/EN-US. + # Apache servers by default impose a further limitation of about + # 250 characters per URL segment (text between slashes). + # mod_pagespeed circumvents this limitation, but if you employ + # proxy servers in your path you may need to re-impose it by + # overriding the setting here. The default setting is 1024 + # characters. + # + # ModPagespeedMaxSegmentLength 250 + + # Uncomment this if you want to prevent mod_pagespeed from combining files + # (e.g. CSS files) across paths + # + # ModPagespeedCombineAcrossPaths off + + # Renaming JavaScript URLs can sometimes break them. With this + # option enabled, mod_pagespeed uses a simple heuristic to decide + # not to rename JavaScript that it thinks is introspective. + # + # You can uncomment this to let mod_pagespeed rename all JS files. + # + # ModPagespeedAvoidRenamingIntrospectiveJavascript off + + # Certain common JavaScript libraries are available from Google, which acts + # as a CDN and allows you to benefit from browser caching if a new visitor + # to your site previously visited another site that makes use of the same + # libraries as you do. Enable the following filter to turn on this feature. + # + # ModPagespeedEnableFilters canonicalize_javascript_libraries + + # The following line configures a library that is recognized by + # canonicalize_javascript_libraries. This will have no effect unless you + # enable this filter (generally by uncommenting the last line in the + # previous stanza). The format is: + # ModPagespeedLibrary bytes md5 canonical_url + # Where bytes and md5 are with respect to the *minified* JS; use + # js_minify --print_size_and_hash to obtain this data. + # Note that we can register multiple hashes for the same canonical url; + # we do this if there are versions available that have already been minified + # with more sophisticated tools. + # + # Additional library configuration can be found in + # pagespeed_libraries.conf included in the distribution. You should add + # new entries here, though, so that file can be automatically upgraded. + # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js + + # Explicitly tell mod_pagespeed to load some resources from disk. + # This will speed up load time and update frequency. + # + # This should only be used for static resources which do not need + # specific headers set or other processing by Apache. + # + # Both URL and filesystem path should specify directories and + # filesystem path must be absolute (for now). + # + # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" + + + # Enables server-side instrumentation and statistics. If this rewriter is + # enabled, then each rewritten HTML page will have instrumentation javacript + # added that sends latency beacons to /mod_pagespeed_beacon. These + # statistics can be accessed at /mod_pagespeed_statistics. You must also + # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers + # below. + # + # ModPagespeedEnableFilters add_instrumentation + + # The add_instrumentation filter sends a beacon after the page onload + # handler is called. The user might navigate to a new URL before this. If + # you enable the following directive, the beacon is sent as part of an + # onbeforeunload handler, for pages where navigation happens before the + # onload event. + # + # ModPagespeedReportUnloadTime on + + # Uncomment the following line so that ModPagespeed will not cache or + # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. + # Note that ModPagespeed always respects Vary: headers on html content. + # ModPagespeedRespectVary on + + # Uncomment the following line if you want to disable statistics entirely. + # + # ModPagespeedStatistics off + + # These handlers are central entry-points into the admin pages. + # By default, pagespeed_admin and pagespeed_global_admin present + # the same data, and differ only when + # ModPagespeedUsePerVHostStatistics is enabled. In that case, + # /pagespeed_global_admin sees aggregated data across all vhosts, + # and the /pagespeed_admin sees data only for a particular vhost. + # + # You may insert other "Allow from" lines to add hosts you want to + # allow to look at generated statistics. Another possibility is + # to comment out the "Order" and "Allow" options from the config + # file, to allow any client that can reach your server to access + # and change server state, such as statistics, caches, and + # messages. This might be appropriate in an experimental setup. + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_admin + + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_global_admin + + + # Enable logging of mod_pagespeed statistics, needed for the console. + ModPagespeedStatisticsLogging on + + # Page /mod_pagespeed_message lets you view the latest messages from + # mod_pagespeed, regardless of log-level in your httpd.conf + # ModPagespeedMessageBufferSize is the maximum number of bytes you would + # like to dump to your /mod_pagespeed_message page at one time, + # its default value is 100k bytes. + # Set it to 0 if you want to disable this feature. + ModPagespeedMessageBufferSize 100000 + diff --git a/templates/ports.conf.j2 b/templates/ports.conf.j2 new file mode 100644 index 00000000..2618436c --- /dev/null +++ b/templates/ports.conf.j2 @@ -0,0 +1,13 @@ +# If you just change the port or add more ports here, you will likely also +# have to change the VirtualHost statement in +# /etc/apache2/sites-enabled/000-default.conf + +Listen 172.16.0.1:8080 + + + Listen 172.16.0.1:443 + + + + Listen 172.16.0.1:443 + diff --git a/templates/privoxy_config.j2 b/templates/privoxy_config.j2 index bb38309c..dd55f0f3 100644 --- a/templates/privoxy_config.j2 +++ b/templates/privoxy_config.j2 @@ -1256,6 +1256,8 @@ enable-proxy-authentication-forwarding 0 # forward / parent-proxy.example.org:8000 # forward ipv6-server.example.org . # forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . +forward / 172.16.0.1:8080 +forward :443 . # # # 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t From e9198705cc9d6b19de02bb8faa364b06be6a7813 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 4 Aug 2016 22:59:02 +0300 Subject: [PATCH 007/769] mod_pagespeed #5 --- features.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features.yml b/features.yml index 12d7625a..3ab5aa01 100644 --- a/features.yml +++ b/features.yml @@ -16,7 +16,7 @@ - name: Loopback is running shell: ifdown lo:100 && ifup lo:100 - #Privoxy + # Privoxy - name: Install privoxy apt: name=privoxy state=latest From 1a4487bac17d882b755cbcf14c62a06400cea0f5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 5 Aug 2016 09:46:19 -0700 Subject: [PATCH 008/769] typo --- run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run b/run index 2b108bb8..55419f0e 100755 --- a/run +++ b/run @@ -3,7 +3,7 @@ echo -n " What provider would you like to use? 1. DigitalOcean - 3. Amazon EC2 + 2. Amazon EC2 Enter the number of your desired provider : " From b1f5fbeab056759ac1ec0b2f88c32496b12e8bad Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 10 Aug 2016 13:22:34 -0400 Subject: [PATCH 009/769] Update README.md --- README.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5eaacfc7..c1f9f105 100644 --- a/README.md +++ b/README.md @@ -26,24 +26,32 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere ### Requirements -* ansible >= 2.2.0 +* ansible >= 2.1.0 * python >= 2.6 -* libselinux-python (for RedHat based distros) * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) * SHell or BASH +* libselinux-python (for RedHat based distros) ### Initial Deployment -**Available cloud providers:** +To install the dependencies on OS X: + +``` +sudo easy_install pip +sudo pip install ansible dopy==0.3.5 +``` + +There are two available cloud providers: * DigitalOcean * Amazon EC2 -Note: For EC2 users, ensure that you setup the required environment variables prior to starting the deploy: +If you want to use Amazon EC2, ensure that you setup the required environment variables prior to starting the deploy: ``` declare -x AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXXXXX" declare -x AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXxx" ``` -Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. Start the deploy and follow the instructions: + +Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. Start the deploy and follow the instructions: ``` ./run From 6cdc74996973a5bf8ebb22a406375b66647ee0c7 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 10 Aug 2016 13:24:36 -0400 Subject: [PATCH 010/769] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1f9f105..73ecc9e4 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ declare -x AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXXXXX" declare -x AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXxx" ``` -Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. Start the deploy and follow the instructions: +Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. + +Start the deploy and follow the instructions: ``` ./run @@ -71,7 +73,7 @@ ansible-playbook users.yml --user=root -i inventory_users Note: For EC2 users, Algo does NOT use EC2 dynamic inventory for user management. Please continue to use users.yml playbook as described below. This may be subject to change in the future. ``` -ansible-playbook users.yml -u ubuntu -i inventory_users +ansible-playbook users.yml --user=ubuntu -i inventory_users ``` ## FAQ From fff70293f15913947c93d9d89e153dea429e01e9 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 11:54:34 +0300 Subject: [PATCH 011/769] Roles enabled --- common.yml | 104 ---------------- deploy.yml | 4 - digitalocean.yml | 114 ++++++++--------- ec2.yml | 43 ++++++- features.yml | 160 ------------------------ inventory_users | 2 + roles/common/handlers/main.yml | 9 ++ roles/common/tasks/main.yml | 70 +++++++++++ roles/digitalocean/handlers/main.yml | 0 roles/digitalocean/tasks/main.yml | 39 ++++++ roles/features/handlers/main.yml | 17 +++ roles/features/tasks/main.yml | 141 +++++++++++++++++++++ roles/security/handlers/main.yml | 8 ++ roles/security/tasks/main.yml | 124 +++++++++++++++++++ roles/vpn/handlers/main.yml | 20 +++ roles/vpn/tasks/main.yml | 144 ++++++++++++++++++++++ security.yml | 136 --------------------- vpn.yml | 175 --------------------------- 18 files changed, 664 insertions(+), 646 deletions(-) delete mode 100644 common.yml delete mode 100644 features.yml create mode 100644 roles/common/handlers/main.yml create mode 100644 roles/common/tasks/main.yml create mode 100644 roles/digitalocean/handlers/main.yml create mode 100644 roles/digitalocean/tasks/main.yml create mode 100644 roles/features/handlers/main.yml create mode 100644 roles/features/tasks/main.yml create mode 100644 roles/security/handlers/main.yml create mode 100644 roles/security/tasks/main.yml create mode 100644 roles/vpn/handlers/main.yml create mode 100644 roles/vpn/tasks/main.yml delete mode 100644 security.yml delete mode 100644 vpn.yml diff --git a/common.yml b/common.yml deleted file mode 100644 index ee2c9526..00000000 --- a/common.yml +++ /dev/null @@ -1,104 +0,0 @@ -# vim:ft=ansible: ---- - -- name: Common tools - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - tasks: - - name: Wait for port 22 to become available - local_action: "wait_for port=22 host={{ inventory_hostname }}" - become: false - - - name: Gather Facts - setup: - - - name: Install software updates - apt: update_cache=yes upgrade=dist - - - name: Check if reboot is required - shell: > - if [[ $(readlink -f /vmlinuz) != /boot/vmlinuz-$(uname -r) ]]; then echo "required"; else echo "no"; fi - args: - executable: /bin/bash - register: reboot_required - - - name: Reboot - shell: sleep 2 && shutdown -r now "Ansible updates triggered" - async: 1 - poll: 0 - when: reboot_required is defined and reboot_required.stdout == 'required' - ignore_errors: true - - - name: Wait for shutdown - local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped timeout=120 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - - - name: Wait until SSH becomes ready... - local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=120 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - - # SSH fixes - - - name: SSH config - lineinfile: dest="{{ item.file }}" regexp="{{ item.regexp }}" line="{{ item.line }}" state=present - with_items: - - { regexp: '^PasswordAuthentication.*', line: 'PasswordAuthentication no', file: '/etc/ssh/sshd_config' } - - { regexp: '^PermitRootLogin.*', line: 'PermitRootLogin without-password', file: '/etc/ssh/sshd_config' } - - { regexp: '^UseDNS.*', line: 'UseDNS no', file: '/etc/ssh/sshd_config' } - - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } - - { regexp: '^MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com', file: '/etc/ssh/sshd_config' } - - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384', file: '/etc/ssh/sshd_config' } - notify: - - restart ssh - - - name: Disable MOTD on login and SSHD - replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" - with_items: - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } - - - name: Install tools - apt: name="{{ item }}" state=latest - with_items: - - git - - screen - - apparmor-utils - - uuid-runtime - - coreutils - - auditd - - rsyslog - - sendmail - - unattended-upgrades - - iptables-persistent - - - name: Configure unattended-upgrades - template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=644 - - - name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=644 - - handlers: - - name: restart auditd - service: name=auditd state=restarted - - - name: restart rsyslog - service: name=rsyslog state=restarted - - - name: restart ssh - service: name=ssh state=restarted - - - name: flush routing cache - shell: echo 1 > /proc/sys/net/ipv4/route/flush - diff --git a/deploy.yml b/deploy.yml index 9e06c583..5d680691 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,8 +1,4 @@ --- - include: "{{ provider }}.yml" -- include: common.yml -- include: security.yml -- include: features.yml -- include: vpn.yml diff --git a/digitalocean.yml b/digitalocean.yml index d46c175a..cce9a471 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -46,67 +46,24 @@ prompt: "Name the vpn server:\n" default: "algo.local" private: no - - tasks: - - name: "Getting your SSH key ID on Digital Ocean..." - digital_ocean: - state: present - command: ssh - name: "{{ do_ssh_name }}" - api_token: "{{ do_access_token }}" - register: do_ssh_key - - - name: "Creating a droplet..." - digital_ocean: - state: present - command: droplet - name: "{{ do_server_name }}" - region_id: "{{ regions[do_region] }}" - size_id: "512mb" - image_id: "ubuntu-16-04-x64" - ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" - unique_name: yes - api_token: "{{ do_access_token }}" - register: do - - - name: Add the droplet to an inventory group - add_host: - name: "{{ do.droplet.ip_address }}" - groups: vpn-host - ansible_ssh_user: root - ansible_python_interpreter: "/usr/bin/python2.7" - - - name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" - - name: Enable IPv6 on the droplet - uri: - url: "https://api.digitalocean.com/v2/droplets/{{ do.droplet.id }}/actions" - method: POST - body: - type: enable_ipv6 - body_format: json - status_code: 201 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - - - name: Get Droplet networks - uri: - url: "https://api.digitalocean.com/v2/droplets/{{ do.droplet.id }}" - method: GET - status_code: 200 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - register: droplet_info + - name: "dns_enabled" + prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no - - name: IPv6 template created - template: src=20-ipv6.cfg.j2 dest=configs/20-ipv6.tmp - with_items: "{{ droplet_info.json.droplet.networks.v6 }}" + - name: "auditd_enabled" + prompt: "Do you want to use auditd ? (Y or N):\n" + default: "Y" + private: no + + roles: + - digitalocean - name: Post-provisioning tasks hosts: vpn-host gather_facts: false - user: root + become: true vars_files: - config.cfg @@ -115,19 +72,54 @@ raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - tasks: + + - name: Enable IPv6 on the droplet + uri: + url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" + method: POST + body: + type: enable_ipv6 + body_format: json + status_code: 201 + HEADER_Authorization: "Bearer {{ do_access_token }}" + HEADER_Content-Type: "application/json" + + - name: Get Droplet networks + uri: + url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" + method: GET + status_code: 200 + HEADER_Authorization: "Bearer {{ do_access_token }}" + HEADER_Content-Type: "application/json" + register: droplet_info + - name: IPv6 configured - copy: src=configs/20-ipv6.tmp dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 + template: src=20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 + with_items: "{{ droplet_info.json.droplet.networks.v6 }}" + notify: + - reload eth0 - name: IPv6 included into the network config lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/20-ipv6.cfg' state=present - - - name: IPV6 is running - shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' + notify: + - reload eth0 + + - meta: flush_handlers - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" become: false + + roles: + - common + - security + - features + - vpn + + handlers: + - name: reload eth0 + shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' + + diff --git a/ec2.yml b/ec2.yml index 486f9913..1aa6c145 100644 --- a/ec2.yml +++ b/ec2.yml @@ -34,7 +34,17 @@ 10. eu-west-1 EU (Ireland) 11. sa-east-1 South America (São Paulo) default: "1" - private: no + private: no + + - name: "dns_enabled" + prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no + + - name: "auditd_enabled" + prompt: "Do you want to use auditd ? (Y or N):\n" + default: "Y" + private: no tasks: @@ -107,14 +117,35 @@ register: ec2 - name: Add new instance to host group - add_host: hostname={{ item.public_ip }} groupname=vpn-host remote_user=ubuntu ansible_python_interpreter="/usr/bin/python2.7" + add_host: + hostname: "{{ item.public_ip }}" + groupname: vpn-host + remote_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + dns_enabled: "{{ dns_enabled }}" + auditd_enabled: " {{ auditd_enabled }}" with_items: "{{ ec2.instances }}" - name: Wait for SSH to come up wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started with_items: "{{ ec2.instances }}" - - - name: accept new ssh fingerprints - shell: ssh-keyscan -H {{ item.public_ip }} >> ~/.ssh/known_hosts - with_items: "{{ ec2.instances }}" + +- name: Post-provisioning tasks + hosts: vpn-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Install prerequisites + raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + - name: Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + + roles: + - common + - security + - features + - vpn diff --git a/features.yml b/features.yml deleted file mode 100644 index 3ab5aa01..00000000 --- a/features.yml +++ /dev/null @@ -1,160 +0,0 @@ ---- - -- name: Other features - hosts: vpn-host - become: true - vars_files: - - config.cfg - - tasks: - - name: Loopback for services configured - template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg - - - name: Loopback included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present - - - name: Loopback is running - shell: ifdown lo:100 && ifup lo:100 - - # Privoxy - - - name: Install privoxy - apt: name=privoxy state=latest - - - name: Privoxy configured - template: src=privoxy_config.j2 dest=/etc/privoxy/config - notify: - - restart privoxy - - - name: Privoxy profile for apparmor configured - template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=600 - notify: - - restart privoxy - - - name: Enforce the privoxy AppArmor policy - shell: aa-enforce usr.sbin.privoxy - - - name: Privoxy enabled and started - service: name=privoxy state=started enabled=yes - - # PageSpeed - - - name: Apache installed - apt: name=apache2 state=latest - - - name: PageSpeed installed for x86_64 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb - when: ansible_architecture == "x86_64" - - - name: PageSpeed installed for i386 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb - when: ansible_architecture != "x86_64" - - - name: PageSpeed configured - template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf - notify: - - restart apache2 - - - name: Modules enabled - apache2_module: state=present name="{{ item }}" - with_items: - - proxy_http - - pagespeed - - cache - - proxy_connect - - proxy_html - - rewrite - notify: - - restart apache2 - - - name: VirtualHost configured for the PageSpeed module - template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf - notify: - - restart apache2 - - - name: Apache ports configured - template: src=ports.conf.j2 dest=/etc/apache2/ports.conf - notify: - - restart apache2 - - # DNS - - - name: Install dnsmasq - apt: name=dnsmasq state=latest - - - name: Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=600 - notify: - - restart dnsmasq - - - name: Enforce the dnsmasq AppArmor policy - shell: aa-enforce usr.sbin.dnsmasq - - - name: Dnsmasq configured - template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf - notify: - - restart dnsmasq - - - name: Adblock script created - copy: src=templates/adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 - when: service_dns is defined and service_dns == "True" - - - name: Adblock script added to cron - cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" - when: service_dns is defined and service_dns == "True" - - - name: Update adblock hosts - shell: > - /opt/adblock.sh - when: service_dns is defined and service_dns == "True" - - - name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network }}" - jump: DNAT - to_destination: 172.16.0.1:53 - notify: - - save iptables - when: service_dns is defined and service_dns == "True" - - - name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network_ipv6 }}" - jump: DNAT - to_destination: fcaa::1:53 - ip_version: ipv6 - notify: - - save iptables - when: service_dns is defined and service_dns == "True" - - - name: Dnsmasq enabled and started - service: name=dnsmasq state=started enabled=yes - when: service_dns is defined and service_dns == "True" - - - name: Dnsmasq disabled and stopped - service: name=dnsmasq state=stopped enabled=no - when: service_dns is defined and service_dns == "False" - - handlers: - - name: restart privoxy - service: name=privoxy state=restarted - - - name: restart dnsmasq - service: name=dnsmasq state=restarted - - - name: restart apparmor - service: name=apparmor state=restarted - - - name: restart apache2 - service: name=apache2 state=restarted - - - name: save iptables - command: service netfilter-persistent save diff --git a/inventory_users b/inventory_users index 8e9e7af6..3f926363 100644 --- a/inventory_users +++ b/inventory_users @@ -1,2 +1,4 @@ [user-management] +146.185.162.155 +37.139.21.209 37.139.0.99 diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 00000000..d7a822d3 --- /dev/null +++ b/roles/common/handlers/main.yml @@ -0,0 +1,9 @@ +- name: restart rsyslog + service: name=rsyslog state=restarted + +- name: restart ssh + service: name=ssh state=restarted + +- name: flush routing cache + shell: echo 1 > /proc/sys/net/ipv4/route/flush + diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 00000000..078726ad --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,70 @@ +--- + +- name: Gather Facts + setup: + +- name: Install software updates + apt: update_cache=yes upgrade=dist + +- name: Check if reboot is required + shell: > + if [[ $(readlink -f /vmlinuz) != /boot/vmlinuz-$(uname -r) ]]; then echo "required"; else echo "no"; fi + args: + executable: /bin/bash + register: reboot_required + +- name: Reboot + shell: sleep 2 && shutdown -r now "Ansible updates triggered" + async: 1 + poll: 0 + when: reboot_required is defined and reboot_required.stdout == 'required' + ignore_errors: true + +- name: Wait for shutdown + local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped timeout=120 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false + +- name: Wait until SSH becomes ready... + local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=120 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false + +# SSH fixes + +- name: SSH config + lineinfile: dest="{{ item.file }}" regexp="{{ item.regexp }}" line="{{ item.line }}" state=present + with_items: + - { regexp: '^PasswordAuthentication.*', line: 'PasswordAuthentication no', file: '/etc/ssh/sshd_config' } + - { regexp: '^PermitRootLogin.*', line: 'PermitRootLogin without-password', file: '/etc/ssh/sshd_config' } + - { regexp: '^UseDNS.*', line: 'UseDNS no', file: '/etc/ssh/sshd_config' } + - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } + - { regexp: '^MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com', file: '/etc/ssh/sshd_config' } + - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384', file: '/etc/ssh/sshd_config' } + notify: + - restart ssh + +- name: Disable MOTD on login and SSHD + replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" + with_items: + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } + +- name: Install tools + apt: name="{{ item }}" state=latest + with_items: + - git + - screen + - apparmor-utils + - uuid-runtime + - coreutils + - rsyslog + - sendmail + - unattended-upgrades + - iptables-persistent + +- name: Configure unattended-upgrades + template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=644 + +- name: Periodic upgrades configured + template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=644 diff --git a/roles/digitalocean/handlers/main.yml b/roles/digitalocean/handlers/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/digitalocean/tasks/main.yml b/roles/digitalocean/tasks/main.yml new file mode 100644 index 00000000..8f0c20f8 --- /dev/null +++ b/roles/digitalocean/tasks/main.yml @@ -0,0 +1,39 @@ +- name: Set the DigitalOcean Access Token fact + set_fact: + do_token: "{{ do_access_token | default( lookup('env', 'DIGITALOCEAN_API_KEY') ) }}" + +- name: "Getting your SSH key ID on Digital Ocean..." + digital_ocean: + state: present + command: ssh + name: "{{ do_ssh_name }}" + api_token: "{{ do_access_token }}" + register: do_ssh_key + +- name: "Creating a droplet..." + digital_ocean: + state: present + command: droplet + name: "{{ do_server_name }}" + region_id: "{{ regions[do_region] }}" + size_id: "512mb" + image_id: "ubuntu-16-04-x64" + ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" + unique_name: yes + api_token: "{{ do_access_token }}" + register: do + +- name: Add the droplet to an inventory group + add_host: + name: "{{ do.droplet.ip_address }}" + groups: vpn-host + ansible_ssh_user: root + ansible_python_interpreter: "/usr/bin/python2.7" + do_access_token: "{{ do_access_token }}" + do_droplet_id: "{{ do.droplet.id }}" + dns_enabled: "{{ dns_enabled }}" + auditd_enabled: " {{ auditd_enabled }}" + +- name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" + diff --git a/roles/features/handlers/main.yml b/roles/features/handlers/main.yml new file mode 100644 index 00000000..284064fc --- /dev/null +++ b/roles/features/handlers/main.yml @@ -0,0 +1,17 @@ +- name: restart privoxy + service: name=privoxy state=restarted + +- name: restart dnsmasq + service: name=dnsmasq state=restarted + +- name: restart apparmor + service: name=apparmor state=restarted + +- name: restart apache2 + service: name=apache2 state=restarted + +- name: save iptables + command: service netfilter-persistent save + +- name: restart loopback + shell: ifdown lo:100 && ifup lo:100 diff --git a/roles/features/tasks/main.yml b/roles/features/tasks/main.yml new file mode 100644 index 00000000..b305b80f --- /dev/null +++ b/roles/features/tasks/main.yml @@ -0,0 +1,141 @@ +- name: Gather Facts + setup: + +- name: Loopback for services configured + template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg + notify: + - restart loopback + +- name: Loopback included into the network config + lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present + notify: + - restart loopback + +- meta: flush_handlers + +# Privoxy + +- name: Privoxy installed + apt: name=privoxy state=latest + +- name: Privoxy configured + template: src=privoxy_config.j2 dest=/etc/privoxy/config + notify: + - restart privoxy + +- name: Privoxy profile for apparmor configured + template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=600 + notify: + - restart privoxy + +- name: Enforce the privoxy AppArmor policy + shell: aa-enforce usr.sbin.privoxy + +- name: Privoxy enabled and started + service: name=privoxy state=started enabled=yes + +# PageSpeed + +- name: Apache installed + apt: name=apache2 state=latest + +- name: PageSpeed installed for x86_64 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb + when: ansible_architecture == "x86_64" + +- name: PageSpeed installed for i386 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb + when: ansible_architecture != "x86_64" + +- name: PageSpeed configured + template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf + notify: + - restart apache2 + +- name: Modules enabled + apache2_module: state=present name="{{ item }}" + with_items: + - proxy_http + - pagespeed + - cache + - proxy_connect + - proxy_html + - rewrite + notify: + - restart apache2 + +- name: VirtualHost configured for the PageSpeed module + template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf + notify: + - restart apache2 + +- name: Apache ports configured + template: src=ports.conf.j2 dest=/etc/apache2/ports.conf + notify: + - restart apache2 + +# DNS + +- name: Dnsmasq installed + apt: name=dnsmasq state=latest + +- name: Dnsmasq profile for apparmor configured + template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=600 + notify: + - restart dnsmasq + +- name: Enforce the dnsmasq AppArmor policy + shell: aa-enforce usr.sbin.dnsmasq + +- name: Dnsmasq configured + template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf + notify: + - restart dnsmasq + +- name: Adblock script created + copy: src=templates/adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Adblock script added to cron + cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Update adblock hosts + shell: > + /opt/adblock.sh + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Forward all DNS requests to the local resolver + iptables: + table: nat + chain: PREROUTING + protocol: udp + destination_port: 53 + source: "{{ vpn_network }}" + jump: DNAT + to_destination: 172.16.0.1:53 + notify: + - save iptables + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Forward all DNS requests to the local resolver + iptables: + table: nat + chain: PREROUTING + protocol: udp + destination_port: 53 + source: "{{ vpn_network_ipv6 }}" + jump: DNAT + to_destination: fcaa::1:53 + ip_version: ipv6 + notify: + - save iptables + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Dnsmasq enabled and started + service: name=dnsmasq state=started enabled=yes + when: dns_enabled is defined and dns_enabled == "Y" + +- name: Dnsmasq disabled and stopped + service: name=dnsmasq state=stopped enabled=no + when: dns_enabled is defined and dns_enabled != "Y" diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml new file mode 100644 index 00000000..248066a9 --- /dev/null +++ b/roles/security/handlers/main.yml @@ -0,0 +1,8 @@ +- name: restart auditd + service: name=auditd state=restarted + +- name: restart rsyslog + service: name=rsyslog state=restarted + +- name: flush routing cache + shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml new file mode 100644 index 00000000..cfe41c5a --- /dev/null +++ b/roles/security/tasks/main.yml @@ -0,0 +1,124 @@ + + +# Using a two-pass approach for checking directories in order to support symlinks. +- name: Find directories for minimizing access + stat: + path: "{{ item }}" + register: minimize_access_directories + with_items: + - '/usr/local/sbin' + - '/usr/local/bin' + - '/usr/sbin' + - '/usr/bin' + - '/sbin' + - '/bin' + +- name: Minimize access + file: path='{{ item.stat.path }}' mode='go-w' recurse=yes + when: item.stat.isdir + with_items: "{{ minimize_access_directories.results }}" + no_log: True + +- name: Change shadow ownership to root and mode to 0600 + file: dest='/etc/shadow' owner=root group=root mode=0600 + +- name: change su-binary to only be accessible to user and group root + file: dest='/bin/su' owner=root group=root mode=0750 + +- name: Collect Use of privileged commands + shell: > + /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' + args: + executable: /bin/bash + register: privileged_programs + +# auditd + +- name: Auditd installed + apt: name=auditd state=latest + when: auditd_enabled is defined and auditd_enabled == 'Y' + +- name: Auditd rules configured + template: src=audit.rules.j2 dest=/etc/audit/audit.rules + notify: + - restart auditd + when: auditd_enabled is defined and auditd_enabled == 'Y' + +- name: Auditd configured + template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf + notify: + - restart auditd + when: auditd_enabled is defined and auditd_enabled == 'Y' + +- name: Enable services + service: name=auditd enabled=yes + when: auditd_enabled is defined and auditd_enabled == 'Y' + +# Rsyslog + +- name: Rsyslog configured + template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf + notify: + - restart rsyslog + +- name: Rsyslog CIS configured + template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 + notify: + - restart rsyslog + +- name: Enable services + service: name=rsyslog enabled=yes + +# Core dumps + +- name: Restrict core dumps (with PAM) + lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present + +- name: Restrict core dumps (with sysctl) + sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + +# Kernel fixes + +- name: Disable Source Routed Packet Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_source_route + - net.ipv4.conf.default.accept_source_route + notify: + - flush routing cache + +- name: Disable ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_redirects + - net.ipv4.conf.default.accept_redirects + +- name: Disable Secure ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.secure_redirects + - net.ipv4.conf.default.secure_redirects + notify: + - flush routing cache + +- name: Enable Bad Error Message Protection + sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + notify: + - flush routing cache + +- name: Enable RFC-recommended Source Route Validation + sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.rp_filter + - net.ipv4.conf.default.rp_filter + notify: + - flush routing cache + +- name: Enable packet forwarding for IPv4 + sysctl: name=net.ipv4.ip_forward value=1 + +- name: Enable packet forwarding for IPv6 + sysctl: name=net.ipv6.conf.all.forwarding value=1 + +- name: Do not send ICMP redirects (we are not a router) + sysctl: name=net.ipv4.conf.all.send_redirects value=0 diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml new file mode 100644 index 00000000..d070b51a --- /dev/null +++ b/roles/vpn/handlers/main.yml @@ -0,0 +1,20 @@ +- name: restart strongswan + service: name=strongswan state=restarted + +- name: restart apparmor + service: name=apparmor state=restarted + +- name: save iptables + command: service netfilter-persistent save + +- name: congrats + debug: + msg: + - "#----------------------------------------------------------------------#" + - "# Congratulations! #" + - "# Your Algo server is running. #" + - "# Config files and certificates are in the ./configs/ directory. #" + - "# Go to https://www.dnsleaktest.com/ after connecting #" + - "# and ensure that all your traffic passes through the VPN. #" + - "# Local DNS resolver and Proxy IP address: 172.16.0.1 #" + - "#----------------------------------------------------------------------#" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml new file mode 100644 index 00000000..478c4370 --- /dev/null +++ b/roles/vpn/tasks/main.yml @@ -0,0 +1,144 @@ +- name: Install StrongSwan + apt: name=strongswan state=latest update_cache=yes + +- name: Enforcing ipsec with apparmor + shell: aa-enforce "{{ item }}" + with_items: + - /usr/lib/ipsec/charon + - /usr/lib/ipsec/lookip + - /usr/lib/ipsec/stroke + notify: + - restart apparmor + +- name: Enable services + service: name={{ item }} enabled=yes + with_items: + - apparmor + - strongswan + - netfilter-persistent + +- name: Configure iptables so IPSec traffic can traverse the tunnel + iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE + notify: + - save iptables + +- name: Configure ip6tables so IPSec traffic can traverse the tunnel + iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE + notify: + - save iptables + +- name: Setup the ipsec.conf file from our template + template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=644 + notify: + - restart strongswan + +- name: Setup the ipsec.secrets file + template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=root group=root mode=600 + notify: + - restart strongswan + +- name: Fetch easy-rsa-ipsec from git + git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git dest="{{ easyrsa_dir }}" + +- name: Setup the vars file from our template + template: src=easy-rsa.vars.j2 dest={{ easyrsa_dir }}/easyrsa3/vars + +- name: Ensure the pki directory is not exist + file: dest={{ easyrsa_dir }}/easyrsa3/pki state=absent + when: easyrsa_reinit_existent == True + +- name: Build the pki enviroments + shell: > + ./easyrsa init-pki && + touch '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' + +- name: Build the CA pair + shell: > + ./easyrsa build-ca nopass && + touch {{ easyrsa_dir }}/easyrsa3/pki/ca_initialized + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/ca_initialized' + notify: + - restart strongswan + +- name: Build the server pair + shell: > + ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ ansible_ssh_host }}' build-server-full {{ ansible_ssh_host }} nopass&& + touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' + notify: + - restart strongswan + +- name: Build the client's pair + shell: > + ./easyrsa build-client-full {{ item }} nopass && + touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + with_items: "{{ users }}" + +- name: Build the client's p12 + shell: > + openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && + touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + with_items: "{{ users }}" + +- name: Copy the CA cert to the strongswan directory + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=root group=root mode=0600 + notify: + - restart strongswan + +- name: Copy the server cert to the strongswan directory + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ ansible_ssh_host }}.crt' dest=/etc/ipsec.d/certs/{{ ansible_ssh_host }}.crt owner=root group=root mode=0600 + notify: + - restart strongswan + +- name: Copy the server key to the strongswan directory + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ ansible_ssh_host }}.key' dest=/etc/ipsec.d/private/{{ ansible_ssh_host }}.key owner=root group=root mode=0600 + notify: + - restart strongswan + +- name: Register p12 PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 + register: PayloadContent + with_items: "{{ users }}" + +- name: Register CA PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 + register: PayloadContentCA + +- name: Build the mobileconfigs + template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True + +- name: Fetch users P12 + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes + with_items: "{{ users }}" + +- name: Fetch users mobileconfig + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ server_name }}_{{ item }}.mobileconfig flat=yes + with_items: "{{ users }}" + +- name: Fetch server CA certificate + fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes + +- name: Add server to the inventory file + local_action: lineinfile dest=inventory_users line="{{ inventory_hostname }}" insertafter='\[user-management\]\n' state=present + become: false + notify: + - congrats diff --git a/security.yml b/security.yml deleted file mode 100644 index b80f3a21..00000000 --- a/security.yml +++ /dev/null @@ -1,136 +0,0 @@ ---- - -- name: Security enhancements - hosts: vpn-host - become: true - vars_files: - - config.cfg - - tasks: - # Using a two-pass approach for checking directories in order to support symlinks. - - name: Find directories for minimizing access - stat: - path: "{{ item }}" - register: minimize_access_directories - with_items: - - '/usr/local/sbin' - - '/usr/local/bin' - - '/usr/sbin' - - '/usr/bin' - - '/sbin' - - '/bin' - - - name: Minimize access - file: path='{{ item.stat.path }}' mode='go-w' recurse=yes - when: item.stat.isdir - with_items: "{{ minimize_access_directories.results }}" - no_log: True - - - name: Change shadow ownership to root and mode to 0600 - file: dest='/etc/shadow' owner=root group=root mode=0600 - - - name: change su-binary to only be accessible to user and group root - file: dest='/bin/su' owner=root group=root mode=0750 - - # auditd - - - name: Collect Use of privileged commands - shell: > - /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' - args: - executable: /bin/bash - register: privileged_programs - - - name: Auditd rules configured - template: src=audit.rules.j2 dest=/etc/audit/audit.rules - notify: - - restart auditd - - - name: Auditd configured - template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf - notify: - - restart auditd - - # Rsyslog - - - name: Rsyslog configured - template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf - notify: - - restart rsyslog - - - name: Rsyslog CIS configured - template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 - notify: - - restart rsyslog - - - name: Enable services - service: name={{ item }} enabled=yes - with_items: - - auditd - - rsyslog - - # Core dumps - - - name: Restrict core dumps (with PAM) - lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present - - - name: Restrict core dumps (with sysctl) - sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - - # Kernel fixes - - - name: Disable Source Routed Packet Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_source_route - - net.ipv4.conf.default.accept_source_route - notify: - - flush routing cache - - - name: Disable ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects - - - name: Disable Secure ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.secure_redirects - - net.ipv4.conf.default.secure_redirects - notify: - - flush routing cache - - - name: Enable Bad Error Message Protection - sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - notify: - - flush routing cache - - - name: Enable RFC-recommended Source Route Validation - sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.rp_filter - - net.ipv4.conf.default.rp_filter - notify: - - flush routing cache - - - name: Enable packet forwarding for IPv4 - sysctl: name=net.ipv4.ip_forward value=1 - - - name: Enable packet forwarding for IPv6 - sysctl: name=net.ipv6.conf.all.forwarding value=1 - - - name: Do not send ICMP redirects (we are not a router) - sysctl: name=net.ipv4.conf.all.send_redirects value=0 - - handlers: - - name: restart auditd - service: name=auditd state=restarted - - - name: restart rsyslog - service: name=rsyslog state=restarted - - - name: flush routing cache - shell: echo 1 > /proc/sys/net/ipv4/route/flush - - diff --git a/vpn.yml b/vpn.yml deleted file mode 100644 index be6ffd4e..00000000 --- a/vpn.yml +++ /dev/null @@ -1,175 +0,0 @@ ---- - -- name: VPN Configuration - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - tasks: - - name: Install StrongSwan - apt: name=strongswan state=latest update_cache=yes - - - name: Enforcing ipsec with apparmor - shell: aa-enforce "{{ item }}" - with_items: - - /usr/lib/ipsec/charon - - /usr/lib/ipsec/lookip - - /usr/lib/ipsec/stroke - notify: - - restart apparmor - - - name: Enable services - service: name={{ item }} enabled=yes - with_items: - - apparmor - - strongswan - - netfilter-persistent - - - name: Configure iptables so IPSec traffic can traverse the tunnel - iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE - notify: - - save iptables - - - name: Configure ip6tables so IPSec traffic can traverse the tunnel - iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - notify: - - save iptables - - - name: Setup the ipsec.conf file from our template - template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=644 - notify: - - restart strongswan - - - name: Setup the ipsec.secrets file - template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=root group=root mode=600 - notify: - - restart strongswan - - - name: Fetch easy-rsa-ipsec from git - git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git dest="{{ easyrsa_dir }}" - - - name: Setup the vars file from our template - template: src=easy-rsa.vars.j2 dest={{ easyrsa_dir }}/easyrsa3/vars - - - name: Ensure the pki directory is not exist - file: dest={{ easyrsa_dir }}/easyrsa3/pki state=absent - when: easyrsa_reinit_existent == True - - - name: Build the pki enviroments - shell: > - ./easyrsa init-pki && - touch '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' - - - name: Build the CA pair - shell: > - ./easyrsa build-ca nopass && - touch {{ easyrsa_dir }}/easyrsa3/pki/ca_initialized - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/ca_initialized' - notify: - - restart strongswan - - - name: Build the server pair - shell: > - ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ ansible_ssh_host }}' build-server-full {{ ansible_ssh_host }} nopass&& - touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' - notify: - - restart strongswan - - - name: Build the client's pair - shell: > - ./easyrsa build-client-full {{ item }} nopass && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' - with_items: "{{ users }}" - - - name: Build the client's p12 - shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' - with_items: "{{ users }}" - - - name: Copy the CA cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=root group=root mode=0600 - notify: - - restart strongswan - - - name: Copy the server cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ ansible_ssh_host }}.crt' dest=/etc/ipsec.d/certs/{{ ansible_ssh_host }}.crt owner=root group=root mode=0600 - notify: - - restart strongswan - - - name: Copy the server key to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ ansible_ssh_host }}.key' dest=/etc/ipsec.d/private/{{ ansible_ssh_host }}.key owner=root group=root mode=0600 - notify: - - restart strongswan - - - name: Register p12 PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 - register: PayloadContent - with_items: "{{ users }}" - - - name: Register CA PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 - register: PayloadContentCA - - - name: Build the mobileconfigs - template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - - - name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes - with_items: "{{ users }}" - - - name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ server_name }}_{{ item }}.mobileconfig flat=yes - with_items: "{{ users }}" - - - name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes - - - name: Add server to the inventory file - local_action: lineinfile dest=inventory_users line="{{ inventory_hostname }}" insertafter='\[user-management\]\n' state=present - become: false - notify: - - congrats - - handlers: - - name: restart strongswan - service: name=strongswan state=restarted - - - name: restart apparmor - service: name=apparmor state=restarted - - - name: save iptables - command: service netfilter-persistent save - - - name: congrats - debug: - msg: - - "#----------------------------------------------------------------------#" - - "# Congratulations! #" - - "# Your Algo server is running. #" - - "# Config files and certificates are in the ./configs/ directory. #" - - "# Go to https://www.dnsleaktest.com/ after connecting #" - - "# and ensure that all your traffic passes through the VPN. #" - - "#----------------------------------------------------------------------#" From 2f66b03880636acbf767e49e290b3bc4a482b6e9 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 22:36:36 +0300 Subject: [PATCH 012/769] EC2 Role; Loggin Role --- README.md | 1 + digitalocean.yml | 3 +- ec2-destroy.yml | 14 + ec2.ini | 187 ---- ec2.py | 1429 ------------------------------ ec2.yml | 108 +-- inventory_users | 2 + roles/ec2/handlers/main.yml | 0 roles/ec2/tasks/main.yml | 78 ++ roles/logging/handlers/main.yml | 2 + roles/logging/tasks/main.yml | 16 + roles/security/handlers/main.yml | 3 - roles/security/tasks/main.yml | 24 - 13 files changed, 139 insertions(+), 1728 deletions(-) create mode 100644 ec2-destroy.yml delete mode 100644 ec2.ini delete mode 100755 ec2.py create mode 100644 roles/ec2/handlers/main.yml create mode 100644 roles/ec2/tasks/main.yml create mode 100644 roles/logging/handlers/main.yml create mode 100644 roles/logging/tasks/main.yml diff --git a/README.md b/README.md index 73ecc9e4..6344c870 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * ansible >= 2.1.0 * python >= 2.6 * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) +* [boto](https://github.com/boto/boto) * SHell or BASH * libselinux-python (for RedHat based distros) diff --git a/digitalocean.yml b/digitalocean.yml index cce9a471..51bf1f20 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -114,7 +114,8 @@ - common - security - features - - vpn + - vpn + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } handlers: - name: reload eth0 diff --git a/ec2-destroy.yml b/ec2-destroy.yml new file mode 100644 index 00000000..95146f6e --- /dev/null +++ b/ec2-destroy.yml @@ -0,0 +1,14 @@ +- name: Create a sandbox instance + hosts: localhost + gather_facts: False + vars_files: + - config.cfg + + tasks: + - name: Terminate instances that were previously launched + ec2: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + region: eu-central-1 + state: 'absent' + instance_ids: "{{ id }}" diff --git a/ec2.ini b/ec2.ini deleted file mode 100644 index a1797b67..00000000 --- a/ec2.ini +++ /dev/null @@ -1,187 +0,0 @@ -# Ansible EC2 external inventory script settings -# - -[ec2] - -# to talk to a private eucalyptus instance uncomment these lines -# and edit edit eucalyptus_host to be the host name of your cloud controller -#eucalyptus = True -#eucalyptus_host = clc.cloud.domain.org - -# AWS regions to make calls to. Set this to 'all' to make request to all regions -# in AWS and merge the results together. Alternatively, set this to a comma -# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2' -regions = us-east-1 -#regions = all -regions_exclude = us-gov-west-1,cn-north-1,ap-south-1 - -# When generating inventory, Ansible needs to know how to address a server. -# Each EC2 instance has a lot of variables associated with it. Here is the list: -# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance -# Below are 2 variables that are used as the address of a server: -# - destination_variable -# - vpc_destination_variable - -# This is the normal destination variable to use. If you are running Ansible -# from outside EC2, then 'public_dns_name' makes the most sense. If you are -# running Ansible from within EC2, then perhaps you want to use the internal -# address, and should set this to 'private_dns_name'. The key of an EC2 tag -# may optionally be used; however the boto instance variables hold precedence -# in the event of a collision. -destination_variable = public_dns_name - -# This allows you to override the inventory_name with an ec2 variable, instead -# of using the destination_variable above. Addressing (aka ansible_ssh_host) -# will still use destination_variable. Tags should be written as 'tag_TAGNAME'. -#hostname_variable = tag_Name - -# For server inside a VPC, using DNS names may not make sense. When an instance -# has 'subnet_id' set, this variable is used. If the subnet is public, setting -# this to 'ip_address' will return the public IP address. For instances in a -# private subnet, this should be set to 'private_ip_address', and Ansible must -# be run from within EC2. The key of an EC2 tag may optionally be used; however -# the boto instance variables hold precedence in the event of a collision. -# WARNING: - instances that are in the private vpc, _without_ public ip address -# will not be listed in the inventory until You set: -# vpc_destination_variable = private_ip_address -vpc_destination_variable = ip_address - -# The following two settings allow flexible ansible host naming based on a -# python format string and a comma-separated list of ec2 tags. Note that: -# -# 1) If the tags referenced are not present for some instances, empty strings -# will be substituted in the format string. -# 2) This overrides both destination_variable and vpc_destination_variable. -# -#destination_format = {0}.{1}.example.com -#destination_format_tags = Name,environment - -# To tag instances on EC2 with the resource records that point to them from -# Route53, uncomment and set 'route53' to True. -route53 = False - -# To exclude RDS instances from the inventory, uncomment and set to False. -#rds = False - -# To exclude ElastiCache instances from the inventory, uncomment and set to False. -#elasticache = False - -# Additionally, you can specify the list of zones to exclude looking up in -# 'route53_excluded_zones' as a comma-separated list. -# route53_excluded_zones = samplezone1.com, samplezone2.com - -# By default, only EC2 instances in the 'running' state are returned. Set -# 'all_instances' to True to return all instances regardless of state. -all_instances = False - -# By default, only EC2 instances in the 'running' state are returned. Specify -# EC2 instance states to return as a comma-separated list. This -# option is overriden when 'all_instances' is True. -# instance_states = pending, running, shutting-down, terminated, stopping, stopped - -# By default, only RDS instances in the 'available' state are returned. Set -# 'all_rds_instances' to True return all RDS instances regardless of state. -all_rds_instances = False - -# By default, only ElastiCache clusters and nodes in the 'available' state -# are returned. Set 'all_elasticache_clusters' and/or 'all_elastic_nodes' -# to True return all ElastiCache clusters and nodes, regardless of state. -# -# Note that all_elasticache_nodes only applies to listed clusters. That means -# if you set all_elastic_clusters to false, no node will be return from -# unavailable clusters, regardless of the state and to what you set for -# all_elasticache_nodes. -all_elasticache_replication_groups = False -all_elasticache_clusters = False -all_elasticache_nodes = False - -# API calls to EC2 are slow. For this reason, we cache the results of an API -# call. Set this to the path you want cache files to be written to. Two files -# will be written to this directory: -# - ansible-ec2.cache -# - ansible-ec2.index -cache_path = ~/.ansible/tmp - -# The number of seconds a cache file is considered valid. After this many -# seconds, a new API call will be made, and the cache file will be updated. -# To disable the cache, set this value to 0 -cache_max_age = 300 - -# Organize groups into a nested/hierarchy instead of a flat namespace. -nested_groups = False - -# Replace - tags when creating groups to avoid issues with ansible -replace_dash_in_groups = True - -# If set to true, any tag of the form "a,b,c" is expanded into a list -# and the results are used to create additional tag_* inventory groups. -expand_csv_tags = False - -# The EC2 inventory output can become very large. To manage its size, -# configure which groups should be created. -group_by_instance_id = True -group_by_region = True -group_by_availability_zone = True -group_by_ami_id = True -group_by_instance_type = True -group_by_key_pair = True -group_by_vpc_id = True -group_by_security_group = True -group_by_tag_keys = True -group_by_tag_none = True -group_by_route53_names = True -group_by_rds_engine = True -group_by_rds_parameter_group = True -group_by_elasticache_engine = True -group_by_elasticache_cluster = True -group_by_elasticache_parameter_group = True -group_by_elasticache_replication_group = True - -# If you only want to include hosts that match a certain regular expression -# pattern_include = staging-* - -# If you want to exclude any hosts that match a certain regular expression -# pattern_exclude = staging-* - -# Instance filters can be used to control which instances are retrieved for -# inventory. For the full list of possible filters, please read the EC2 API -# docs: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html#query-DescribeInstances-filters -# Filters are key/value pairs separated by '=', to list multiple filters use -# a list separated by commas. See examples below. - -# Retrieve only instances with (key=value) env=staging tag -# instance_filters = tag:env=staging - -# Retrieve only instances with role=webservers OR role=dbservers tag -# instance_filters = tag:role=webservers,tag:role=dbservers - -# Retrieve only t1.micro instances OR instances with tag env=staging -# instance_filters = instance-type=t1.micro,tag:env=staging - -# You can use wildcards in filter values also. Below will list instances which -# tag Name value matches webservers1* -# (ex. webservers15, webservers1a, webservers123 etc) -# instance_filters = tag:Name=webservers1* - -# A boto configuration profile may be used to separate out credentials -# see http://boto.readthedocs.org/en/latest/boto_config_tut.html -# boto_profile = some-boto-profile-name - - -[credentials] - -# The AWS credentials can optionally be specified here. Credentials specified -# here are ignored if the environment variable AWS_ACCESS_KEY_ID or -# AWS_PROFILE is set, or if the boto_profile property above is set. -# -# Supplying AWS credentials here is not recommended, as it introduces -# non-trivial security concerns. When going down this route, please make sure -# to set access permissions for this file correctly, e.g. handle it the same -# way as you would a private SSH key. -# -# Unlike the boto and AWS configure files, this section does not support -# profiles. -# -# aws_access_key_id = AXXXXXXXXXXXXXX -# aws_secret_access_key = XXXXXXXXXXXXXXXXXXX -# aws_security_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/ec2.py b/ec2.py deleted file mode 100755 index 9c565fdf..00000000 --- a/ec2.py +++ /dev/null @@ -1,1429 +0,0 @@ -#!/usr/bin/env python - -''' -EC2 external inventory script -================================= - -Generates inventory that Ansible can understand by making API request to -AWS EC2 using the Boto library. - -NOTE: This script assumes Ansible is being executed where the environment -variables needed for Boto have already been set: - export AWS_ACCESS_KEY_ID='AK123' - export AWS_SECRET_ACCESS_KEY='abc123' - -This script also assumes there is an ec2.ini file alongside it. To specify a -different path to ec2.ini, define the EC2_INI_PATH environment variable: - - export EC2_INI_PATH=/path/to/my_ec2.ini - -If you're using eucalyptus you need to set the above variables and -you need to define: - - export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus - -If you're using boto profiles (requires boto>=2.24.0) you can choose a profile -using the --boto-profile command line argument (e.g. ec2.py --boto-profile prod) or using -the AWS_PROFILE variable: - - AWS_PROFILE=prod ansible-playbook -i ec2.py myplaybook.yml - -For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html - -When run against a specific host, this script returns the following variables: - - ec2_ami_launch_index - - ec2_architecture - - ec2_association - - ec2_attachTime - - ec2_attachment - - ec2_attachmentId - - ec2_client_token - - ec2_deleteOnTermination - - ec2_description - - ec2_deviceIndex - - ec2_dns_name - - ec2_eventsSet - - ec2_group_name - - ec2_hypervisor - - ec2_id - - ec2_image_id - - ec2_instanceState - - ec2_instance_type - - ec2_ipOwnerId - - ec2_ip_address - - ec2_item - - ec2_kernel - - ec2_key_name - - ec2_launch_time - - ec2_monitored - - ec2_monitoring - - ec2_networkInterfaceId - - ec2_ownerId - - ec2_persistent - - ec2_placement - - ec2_platform - - ec2_previous_state - - ec2_private_dns_name - - ec2_private_ip_address - - ec2_publicIp - - ec2_public_dns_name - - ec2_ramdisk - - ec2_reason - - ec2_region - - ec2_requester_id - - ec2_root_device_name - - ec2_root_device_type - - ec2_security_group_ids - - ec2_security_group_names - - ec2_shutdown_state - - ec2_sourceDestCheck - - ec2_spot_instance_request_id - - ec2_state - - ec2_state_code - - ec2_state_reason - - ec2_status - - ec2_subnet_id - - ec2_tenancy - - ec2_virtualization_type - - ec2_vpc_id - -These variables are pulled out of a boto.ec2.instance object. There is a lack of -consistency with variable spellings (camelCase and underscores) since this -just loops through all variables the object exposes. It is preferred to use the -ones with underscores when multiple exist. - -In addition, if an instance has AWS Tags associated with it, each tag is a new -variable named: - - ec2_tag_[Key] = [Value] - -Security groups are comma-separated in 'ec2_security_group_ids' and -'ec2_security_group_names'. -''' - -# (c) 2012, Peter Sankauskas -# -# This file is part of Ansible, -# -# Ansible 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. -# -# Ansible 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 Ansible. If not, see . - -###################################################################### - -import sys -import os -import argparse -import re -from time import time -import boto -from boto import ec2 -from boto import rds -from boto import elasticache -from boto import route53 -import six - -from six.moves import configparser -from collections import defaultdict - -try: - import json -except ImportError: - import simplejson as json - - -class Ec2Inventory(object): - - def _empty_inventory(self): - return {"_meta" : {"hostvars" : {}}} - - def __init__(self): - ''' Main execution path ''' - - # Inventory grouped by instance IDs, tags, security groups, regions, - # and availability zones - self.inventory = self._empty_inventory() - - # Index of hostname (address) to instance ID - self.index = {} - - # Boto profile to use (if any) - self.boto_profile = None - - # AWS credentials. - self.credentials = {} - - # Read settings and parse CLI arguments - self.parse_cli_args() - self.read_settings() - - # Make sure that profile_name is not passed at all if not set - # as pre 2.24 boto will fall over otherwise - if self.boto_profile: - if not hasattr(boto.ec2.EC2Connection, 'profile_name'): - self.fail_with_error("boto version must be >= 2.24 to use profile") - - # Cache - if self.args.refresh_cache: - self.do_api_calls_update_cache() - elif not self.is_cache_valid(): - self.do_api_calls_update_cache() - - # Data to print - if self.args.host: - data_to_print = self.get_host_info() - - elif self.args.list: - # Display list of instances for inventory - if self.inventory == self._empty_inventory(): - data_to_print = self.get_inventory_from_cache() - else: - data_to_print = self.json_format_dict(self.inventory, True) - - print(data_to_print) - - - def is_cache_valid(self): - ''' Determines if the cache files have expired, or if it is still valid ''' - - if os.path.isfile(self.cache_path_cache): - mod_time = os.path.getmtime(self.cache_path_cache) - current_time = time() - if (mod_time + self.cache_max_age) > current_time: - if os.path.isfile(self.cache_path_index): - return True - - return False - - - def read_settings(self): - ''' Reads the settings from the ec2.ini file ''' - if six.PY3: - config = configparser.ConfigParser() - else: - config = configparser.SafeConfigParser() - ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini') - ec2_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('EC2_INI_PATH', ec2_default_ini_path))) - config.read(ec2_ini_path) - - # is eucalyptus? - self.eucalyptus_host = None - self.eucalyptus = False - if config.has_option('ec2', 'eucalyptus'): - self.eucalyptus = config.getboolean('ec2', 'eucalyptus') - if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'): - self.eucalyptus_host = config.get('ec2', 'eucalyptus_host') - - # Regions - self.regions = [] - configRegions = config.get('ec2', 'regions') - configRegions_exclude = config.get('ec2', 'regions_exclude') - if (configRegions == 'all'): - if self.eucalyptus_host: - self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name, **self.credentials) - else: - for regionInfo in ec2.regions(): - if regionInfo.name not in configRegions_exclude: - self.regions.append(regionInfo.name) - else: - self.regions = configRegions.split(",") - - # Destination addresses - self.destination_variable = config.get('ec2', 'destination_variable') - self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable') - - if config.has_option('ec2', 'hostname_variable'): - self.hostname_variable = config.get('ec2', 'hostname_variable') - else: - self.hostname_variable = None - - if config.has_option('ec2', 'destination_format') and \ - config.has_option('ec2', 'destination_format_tags'): - self.destination_format = config.get('ec2', 'destination_format') - self.destination_format_tags = config.get('ec2', 'destination_format_tags').split(',') - else: - self.destination_format = None - self.destination_format_tags = None - - # Route53 - self.route53_enabled = config.getboolean('ec2', 'route53') - self.route53_excluded_zones = [] - if config.has_option('ec2', 'route53_excluded_zones'): - self.route53_excluded_zones.extend( - config.get('ec2', 'route53_excluded_zones', '').split(',')) - - # Include RDS instances? - self.rds_enabled = True - if config.has_option('ec2', 'rds'): - self.rds_enabled = config.getboolean('ec2', 'rds') - - # Include ElastiCache instances? - self.elasticache_enabled = True - if config.has_option('ec2', 'elasticache'): - self.elasticache_enabled = config.getboolean('ec2', 'elasticache') - - # Return all EC2 instances? - if config.has_option('ec2', 'all_instances'): - self.all_instances = config.getboolean('ec2', 'all_instances') - else: - self.all_instances = False - - # Instance states to be gathered in inventory. Default is 'running'. - # Setting 'all_instances' to 'yes' overrides this option. - ec2_valid_instance_states = [ - 'pending', - 'running', - 'shutting-down', - 'terminated', - 'stopping', - 'stopped' - ] - self.ec2_instance_states = [] - if self.all_instances: - self.ec2_instance_states = ec2_valid_instance_states - elif config.has_option('ec2', 'instance_states'): - for instance_state in config.get('ec2', 'instance_states').split(','): - instance_state = instance_state.strip() - if instance_state not in ec2_valid_instance_states: - continue - self.ec2_instance_states.append(instance_state) - else: - self.ec2_instance_states = ['running'] - - # Return all RDS instances? (if RDS is enabled) - if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled: - self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances') - else: - self.all_rds_instances = False - - # Return all ElastiCache replication groups? (if ElastiCache is enabled) - if config.has_option('ec2', 'all_elasticache_replication_groups') and self.elasticache_enabled: - self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups') - else: - self.all_elasticache_replication_groups = False - - # Return all ElastiCache clusters? (if ElastiCache is enabled) - if config.has_option('ec2', 'all_elasticache_clusters') and self.elasticache_enabled: - self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters') - else: - self.all_elasticache_clusters = False - - # Return all ElastiCache nodes? (if ElastiCache is enabled) - if config.has_option('ec2', 'all_elasticache_nodes') and self.elasticache_enabled: - self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes') - else: - self.all_elasticache_nodes = False - - # boto configuration profile (prefer CLI argument) - self.boto_profile = self.args.boto_profile - if config.has_option('ec2', 'boto_profile') and not self.boto_profile: - self.boto_profile = config.get('ec2', 'boto_profile') - - # AWS credentials (prefer environment variables) - if not (self.boto_profile or os.environ.get('AWS_ACCESS_KEY_ID') or - os.environ.get('AWS_PROFILE')): - if config.has_option('credentials', 'aws_access_key_id'): - aws_access_key_id = config.get('credentials', 'aws_access_key_id') - else: - aws_access_key_id = None - if config.has_option('credentials', 'aws_secret_access_key'): - aws_secret_access_key = config.get('credentials', 'aws_secret_access_key') - else: - aws_secret_access_key = None - if config.has_option('credentials', 'aws_security_token'): - aws_security_token = config.get('credentials', 'aws_security_token') - else: - aws_security_token = None - if aws_access_key_id: - self.credentials = { - 'aws_access_key_id': aws_access_key_id, - 'aws_secret_access_key': aws_secret_access_key - } - if aws_security_token: - self.credentials['security_token'] = aws_security_token - - # Cache related - cache_dir = os.path.expanduser(config.get('ec2', 'cache_path')) - if self.boto_profile: - cache_dir = os.path.join(cache_dir, 'profile_' + self.boto_profile) - if not os.path.exists(cache_dir): - os.makedirs(cache_dir) - - cache_name = 'ansible-ec2' - aws_profile = lambda: (self.boto_profile or - os.environ.get('AWS_PROFILE') or - os.environ.get('AWS_ACCESS_KEY_ID') or - self.credentials.get('aws_access_key_id', None)) - if aws_profile(): - cache_name = '%s-%s' % (cache_name, aws_profile()) - self.cache_path_cache = cache_dir + "/%s.cache" % cache_name - self.cache_path_index = cache_dir + "/%s.index" % cache_name - self.cache_max_age = config.getint('ec2', 'cache_max_age') - - if config.has_option('ec2', 'expand_csv_tags'): - self.expand_csv_tags = config.getboolean('ec2', 'expand_csv_tags') - else: - self.expand_csv_tags = False - - # Configure nested groups instead of flat namespace. - if config.has_option('ec2', 'nested_groups'): - self.nested_groups = config.getboolean('ec2', 'nested_groups') - else: - self.nested_groups = False - - # Replace dash or not in group names - if config.has_option('ec2', 'replace_dash_in_groups'): - self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups') - else: - self.replace_dash_in_groups = True - - # Configure which groups should be created. - group_by_options = [ - 'group_by_instance_id', - 'group_by_region', - 'group_by_availability_zone', - 'group_by_ami_id', - 'group_by_instance_type', - 'group_by_key_pair', - 'group_by_vpc_id', - 'group_by_security_group', - 'group_by_tag_keys', - 'group_by_tag_none', - 'group_by_route53_names', - 'group_by_rds_engine', - 'group_by_rds_parameter_group', - 'group_by_elasticache_engine', - 'group_by_elasticache_cluster', - 'group_by_elasticache_parameter_group', - 'group_by_elasticache_replication_group', - ] - for option in group_by_options: - if config.has_option('ec2', option): - setattr(self, option, config.getboolean('ec2', option)) - else: - setattr(self, option, True) - - # Do we need to just include hosts that match a pattern? - try: - pattern_include = config.get('ec2', 'pattern_include') - if pattern_include and len(pattern_include) > 0: - self.pattern_include = re.compile(pattern_include) - else: - self.pattern_include = None - except configparser.NoOptionError: - self.pattern_include = None - - # Do we need to exclude hosts that match a pattern? - try: - pattern_exclude = config.get('ec2', 'pattern_exclude'); - if pattern_exclude and len(pattern_exclude) > 0: - self.pattern_exclude = re.compile(pattern_exclude) - else: - self.pattern_exclude = None - except configparser.NoOptionError: - self.pattern_exclude = None - - # Instance filters (see boto and EC2 API docs). Ignore invalid filters. - self.ec2_instance_filters = defaultdict(list) - if config.has_option('ec2', 'instance_filters'): - - filters = [f for f in config.get('ec2', 'instance_filters').split(',') if f] - - for instance_filter in filters: - instance_filter = instance_filter.strip() - if not instance_filter or '=' not in instance_filter: - continue - filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)] - if not filter_key: - continue - self.ec2_instance_filters[filter_key].append(filter_value) - - def parse_cli_args(self): - ''' Command line argument processing ''' - - parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2') - parser.add_argument('--list', action='store_true', default=True, - help='List instances (default: True)') - parser.add_argument('--host', action='store', - help='Get all the variables about a specific instance') - parser.add_argument('--refresh-cache', action='store_true', default=False, - help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)') - parser.add_argument('--profile', '--boto-profile', action='store', dest='boto_profile', - help='Use boto profile for connections to EC2') - self.args = parser.parse_args() - - - def do_api_calls_update_cache(self): - ''' Do API calls to each region, and save data in cache files ''' - - if self.route53_enabled: - self.get_route53_records() - - for region in self.regions: - self.get_instances_by_region(region) - if self.rds_enabled: - self.get_rds_instances_by_region(region) - if self.elasticache_enabled: - self.get_elasticache_clusters_by_region(region) - self.get_elasticache_replication_groups_by_region(region) - - self.write_to_cache(self.inventory, self.cache_path_cache) - self.write_to_cache(self.index, self.cache_path_index) - - def connect(self, region): - ''' create connection to api server''' - if self.eucalyptus: - conn = boto.connect_euca(host=self.eucalyptus_host, **self.credentials) - conn.APIVersion = '2010-08-31' - else: - conn = self.connect_to_aws(ec2, region) - return conn - - def boto_fix_security_token_in_profile(self, connect_args): - ''' monkey patch for boto issue boto/boto#2100 ''' - profile = 'profile ' + self.boto_profile - if boto.config.has_option(profile, 'aws_security_token'): - connect_args['security_token'] = boto.config.get(profile, 'aws_security_token') - return connect_args - - def connect_to_aws(self, module, region): - connect_args = self.credentials - - # only pass the profile name if it's set (as it is not supported by older boto versions) - if self.boto_profile: - connect_args['profile_name'] = self.boto_profile - self.boto_fix_security_token_in_profile(connect_args) - - conn = module.connect_to_region(region, **connect_args) - # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported - if conn is None: - self.fail_with_error("region name: %s likely not supported, or AWS is down. connection to region failed." % region) - return conn - - def get_instances_by_region(self, region): - ''' Makes an AWS EC2 API call to the list of instances in a particular - region ''' - - try: - conn = self.connect(region) - reservations = [] - if self.ec2_instance_filters: - for filter_key, filter_values in self.ec2_instance_filters.items(): - reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values })) - else: - reservations = conn.get_all_instances() - - # Pull the tags back in a second step - # AWS are on record as saying that the tags fetched in the first `get_all_instances` request are not - # reliable and may be missing, and the only way to guarantee they are there is by calling `get_all_tags` - instance_ids = [] - for reservation in reservations: - instance_ids.extend([instance.id for instance in reservation.instances]) - - max_filter_value = 199 - tags = [] - for i in range(0, len(instance_ids), max_filter_value): - tags.extend(conn.get_all_tags(filters={'resource-type': 'instance', 'resource-id': instance_ids[i:i+max_filter_value]})) - - tags_by_instance_id = defaultdict(dict) - for tag in tags: - tags_by_instance_id[tag.res_id][tag.name] = tag.value - - for reservation in reservations: - for instance in reservation.instances: - instance.tags = tags_by_instance_id[instance.id] - self.add_instance(instance, region) - - except boto.exception.BotoServerError as e: - if e.error_code == 'AuthFailure': - error = self.get_auth_error_message() - else: - backend = 'Eucalyptus' if self.eucalyptus else 'AWS' - error = "Error connecting to %s backend.\n%s" % (backend, e.message) - self.fail_with_error(error, 'getting EC2 instances') - - def get_rds_instances_by_region(self, region): - ''' Makes an AWS API call to the list of RDS instances in a particular - region ''' - - try: - conn = self.connect_to_aws(rds, region) - if conn: - marker = None - while True: - instances = conn.get_all_dbinstances(marker=marker) - marker = instances.marker - for instance in instances: - self.add_rds_instance(instance, region) - if not marker: - break - except boto.exception.BotoServerError as e: - error = e.reason - - if e.error_code == 'AuthFailure': - error = self.get_auth_error_message() - if not e.reason == "Forbidden": - error = "Looks like AWS RDS is down:\n%s" % e.message - self.fail_with_error(error, 'getting RDS instances') - - def get_elasticache_clusters_by_region(self, region): - ''' Makes an AWS API call to the list of ElastiCache clusters (with - nodes' info) in a particular region.''' - - # ElastiCache boto module doesn't provide a get_all_intances method, - # that's why we need to call describe directly (it would be called by - # the shorthand method anyway...) - try: - conn = self.connect_to_aws(elasticache, region) - if conn: - # show_cache_node_info = True - # because we also want nodes' information - response = conn.describe_cache_clusters(None, None, None, True) - - except boto.exception.BotoServerError as e: - error = e.reason - - if e.error_code == 'AuthFailure': - error = self.get_auth_error_message() - if not e.reason == "Forbidden": - error = "Looks like AWS ElastiCache is down:\n%s" % e.message - self.fail_with_error(error, 'getting ElastiCache clusters') - - try: - # Boto also doesn't provide wrapper classes to CacheClusters or - # CacheNodes. Because of that wo can't make use of the get_list - # method in the AWSQueryConnection. Let's do the work manually - clusters = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters'] - - except KeyError as e: - error = "ElastiCache query to AWS failed (unexpected format)." - self.fail_with_error(error, 'getting ElastiCache clusters') - - for cluster in clusters: - self.add_elasticache_cluster(cluster, region) - - def get_elasticache_replication_groups_by_region(self, region): - ''' Makes an AWS API call to the list of ElastiCache replication groups - in a particular region.''' - - # ElastiCache boto module doesn't provide a get_all_intances method, - # that's why we need to call describe directly (it would be called by - # the shorthand method anyway...) - try: - conn = self.connect_to_aws(elasticache, region) - if conn: - response = conn.describe_replication_groups() - - except boto.exception.BotoServerError as e: - error = e.reason - - if e.error_code == 'AuthFailure': - error = self.get_auth_error_message() - if not e.reason == "Forbidden": - error = "Looks like AWS ElastiCache [Replication Groups] is down:\n%s" % e.message - self.fail_with_error(error, 'getting ElastiCache clusters') - - try: - # Boto also doesn't provide wrapper classes to ReplicationGroups - # Because of that wo can't make use of the get_list method in the - # AWSQueryConnection. Let's do the work manually - replication_groups = response['DescribeReplicationGroupsResponse']['DescribeReplicationGroupsResult']['ReplicationGroups'] - - except KeyError as e: - error = "ElastiCache [Replication Groups] query to AWS failed (unexpected format)." - self.fail_with_error(error, 'getting ElastiCache clusters') - - for replication_group in replication_groups: - self.add_elasticache_replication_group(replication_group, region) - - def get_auth_error_message(self): - ''' create an informative error message if there is an issue authenticating''' - errors = ["Authentication error retrieving ec2 inventory."] - if None in [os.environ.get('AWS_ACCESS_KEY_ID'), os.environ.get('AWS_SECRET_ACCESS_KEY')]: - errors.append(' - No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment vars found') - else: - errors.append(' - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment vars found but may not be correct') - - boto_paths = ['/etc/boto.cfg', '~/.boto', '~/.aws/credentials'] - boto_config_found = list(p for p in boto_paths if os.path.isfile(os.path.expanduser(p))) - if len(boto_config_found) > 0: - errors.append(" - Boto configs found at '%s', but the credentials contained may not be correct" % ', '.join(boto_config_found)) - else: - errors.append(" - No Boto config found at any expected location '%s'" % ', '.join(boto_paths)) - - return '\n'.join(errors) - - def fail_with_error(self, err_msg, err_operation=None): - '''log an error to std err for ansible-playbook to consume and exit''' - if err_operation: - err_msg = 'ERROR: "{err_msg}", while: {err_operation}'.format( - err_msg=err_msg, err_operation=err_operation) - sys.stderr.write(err_msg) - sys.exit(1) - - def get_instance(self, region, instance_id): - conn = self.connect(region) - - reservations = conn.get_all_instances([instance_id]) - for reservation in reservations: - for instance in reservation.instances: - return instance - - def add_instance(self, instance, region): - ''' Adds an instance to the inventory and index, as long as it is - addressable ''' - - # Only return instances with desired instance states - if instance.state not in self.ec2_instance_states: - return - - # Select the best destination address - if self.destination_format and self.destination_format_tags: - dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, '') for tag in self.destination_format_tags ]) - elif instance.subnet_id: - dest = getattr(instance, self.vpc_destination_variable, None) - if dest is None: - dest = getattr(instance, 'tags').get(self.vpc_destination_variable, None) - else: - dest = getattr(instance, self.destination_variable, None) - if dest is None: - dest = getattr(instance, 'tags').get(self.destination_variable, None) - - if not dest: - # Skip instances we cannot address (e.g. private VPC subnet) - return - - # Set the inventory name - hostname = None - if self.hostname_variable: - if self.hostname_variable.startswith('tag_'): - hostname = instance.tags.get(self.hostname_variable[4:], None) - else: - hostname = getattr(instance, self.hostname_variable) - - # If we can't get a nice hostname, use the destination address - if not hostname: - hostname = dest - else: - hostname = self.to_safe(hostname).lower() - - # if we only want to include hosts that match a pattern, skip those that don't - if self.pattern_include and not self.pattern_include.match(hostname): - return - - # if we need to exclude hosts that match a pattern, skip those - if self.pattern_exclude and self.pattern_exclude.match(hostname): - return - - # Add to index - self.index[hostname] = [region, instance.id] - - # Inventory: Group by instance ID (always a group of 1) - if self.group_by_instance_id: - self.inventory[instance.id] = [hostname] - if self.nested_groups: - self.push_group(self.inventory, 'instances', instance.id) - - # Inventory: Group by region - if self.group_by_region: - self.push(self.inventory, region, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - - # Inventory: Group by availability zone - if self.group_by_availability_zone: - self.push(self.inventory, instance.placement, hostname) - if self.nested_groups: - if self.group_by_region: - self.push_group(self.inventory, region, instance.placement) - self.push_group(self.inventory, 'zones', instance.placement) - - # Inventory: Group by Amazon Machine Image (AMI) ID - if self.group_by_ami_id: - ami_id = self.to_safe(instance.image_id) - self.push(self.inventory, ami_id, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'images', ami_id) - - # Inventory: Group by instance type - if self.group_by_instance_type: - type_name = self.to_safe('type_' + instance.instance_type) - self.push(self.inventory, type_name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) - - # Inventory: Group by key pair - if self.group_by_key_pair and instance.key_name: - key_name = self.to_safe('key_' + instance.key_name) - self.push(self.inventory, key_name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'keys', key_name) - - # Inventory: Group by VPC - if self.group_by_vpc_id and instance.vpc_id: - vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id) - self.push(self.inventory, vpc_id_name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'vpcs', vpc_id_name) - - # Inventory: Group by security group - if self.group_by_security_group: - try: - for group in instance.groups: - key = self.to_safe("security_group_" + group.name) - self.push(self.inventory, key, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) - except AttributeError: - self.fail_with_error('\n'.join(['Package boto seems a bit older.', - 'Please upgrade boto >= 2.3.0.'])) - - # Inventory: Group by tag keys - if self.group_by_tag_keys: - for k, v in instance.tags.items(): - if self.expand_csv_tags and v and ',' in v: - values = map(lambda x: x.strip(), v.split(',')) - else: - values = [v] - - for v in values: - if v: - key = self.to_safe("tag_" + k + "=" + v) - else: - key = self.to_safe("tag_" + k) - self.push(self.inventory, key, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) - if v: - self.push_group(self.inventory, self.to_safe("tag_" + k), key) - - # Inventory: Group by Route53 domain names if enabled - if self.route53_enabled and self.group_by_route53_names: - route53_names = self.get_instance_route53_names(instance) - for name in route53_names: - self.push(self.inventory, name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'route53', name) - - # Global Tag: instances without tags - if self.group_by_tag_none and len(instance.tags) == 0: - self.push(self.inventory, 'tag_none', hostname) - if self.nested_groups: - self.push_group(self.inventory, 'tags', 'tag_none') - - # Global Tag: tag all EC2 instances - self.push(self.inventory, 'ec2', hostname) - - self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance) - self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest - - - def add_rds_instance(self, instance, region): - ''' Adds an RDS instance to the inventory and index, as long as it is - addressable ''' - - # Only want available instances unless all_rds_instances is True - if not self.all_rds_instances and instance.status != 'available': - return - - # Select the best destination address - dest = instance.endpoint[0] - - if not dest: - # Skip instances we cannot address (e.g. private VPC subnet) - return - - # Set the inventory name - hostname = None - if self.hostname_variable: - if self.hostname_variable.startswith('tag_'): - hostname = instance.tags.get(self.hostname_variable[4:], None) - else: - hostname = getattr(instance, self.hostname_variable) - - # If we can't get a nice hostname, use the destination address - if not hostname: - hostname = dest - - hostname = self.to_safe(hostname).lower() - - # Add to index - self.index[hostname] = [region, instance.id] - - # Inventory: Group by instance ID (always a group of 1) - if self.group_by_instance_id: - self.inventory[instance.id] = [hostname] - if self.nested_groups: - self.push_group(self.inventory, 'instances', instance.id) - - # Inventory: Group by region - if self.group_by_region: - self.push(self.inventory, region, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - - # Inventory: Group by availability zone - if self.group_by_availability_zone: - self.push(self.inventory, instance.availability_zone, hostname) - if self.nested_groups: - if self.group_by_region: - self.push_group(self.inventory, region, instance.availability_zone) - self.push_group(self.inventory, 'zones', instance.availability_zone) - - # Inventory: Group by instance type - if self.group_by_instance_type: - type_name = self.to_safe('type_' + instance.instance_class) - self.push(self.inventory, type_name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) - - # Inventory: Group by VPC - if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id: - vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id) - self.push(self.inventory, vpc_id_name, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'vpcs', vpc_id_name) - - # Inventory: Group by security group - if self.group_by_security_group: - try: - if instance.security_group: - key = self.to_safe("security_group_" + instance.security_group.name) - self.push(self.inventory, key, hostname) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) - - except AttributeError: - self.fail_with_error('\n'.join(['Package boto seems a bit older.', - 'Please upgrade boto >= 2.3.0.'])) - - - # Inventory: Group by engine - if self.group_by_rds_engine: - self.push(self.inventory, self.to_safe("rds_" + instance.engine), hostname) - if self.nested_groups: - self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) - - # Inventory: Group by parameter group - if self.group_by_rds_parameter_group: - self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), hostname) - if self.nested_groups: - self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) - - # Global Tag: all RDS instances - self.push(self.inventory, 'rds', hostname) - - self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance) - self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest - - def add_elasticache_cluster(self, cluster, region): - ''' Adds an ElastiCache cluster to the inventory and index, as long as - it's nodes are addressable ''' - - # Only want available clusters unless all_elasticache_clusters is True - if not self.all_elasticache_clusters and cluster['CacheClusterStatus'] != 'available': - return - - # Select the best destination address - if 'ConfigurationEndpoint' in cluster and cluster['ConfigurationEndpoint']: - # Memcached cluster - dest = cluster['ConfigurationEndpoint']['Address'] - is_redis = False - else: - # Redis sigle node cluster - # Because all Redis clusters are single nodes, we'll merge the - # info from the cluster with info about the node - dest = cluster['CacheNodes'][0]['Endpoint']['Address'] - is_redis = True - - if not dest: - # Skip clusters we cannot address (e.g. private VPC subnet) - return - - # Add to index - self.index[dest] = [region, cluster['CacheClusterId']] - - # Inventory: Group by instance ID (always a group of 1) - if self.group_by_instance_id: - self.inventory[cluster['CacheClusterId']] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'instances', cluster['CacheClusterId']) - - # Inventory: Group by region - if self.group_by_region and not is_redis: - self.push(self.inventory, region, dest) - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - - # Inventory: Group by availability zone - if self.group_by_availability_zone and not is_redis: - self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) - if self.nested_groups: - if self.group_by_region: - self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) - self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) - - # Inventory: Group by node type - if self.group_by_instance_type and not is_redis: - type_name = self.to_safe('type_' + cluster['CacheNodeType']) - self.push(self.inventory, type_name, dest) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) - - # Inventory: Group by VPC (information not available in the current - # AWS API version for ElastiCache) - - # Inventory: Group by security group - if self.group_by_security_group and not is_redis: - - # Check for the existence of the 'SecurityGroups' key and also if - # this key has some value. When the cluster is not placed in a SG - # the query can return None here and cause an error. - if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: - for security_group in cluster['SecurityGroups']: - key = self.to_safe("security_group_" + security_group['SecurityGroupId']) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) - - # Inventory: Group by engine - if self.group_by_elasticache_engine and not is_redis: - self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) - if self.nested_groups: - self.push_group(self.inventory, 'elasticache_engines', self.to_safe(cluster['Engine'])) - - # Inventory: Group by parameter group - if self.group_by_elasticache_parameter_group: - self.push(self.inventory, self.to_safe("elasticache_parameter_group_" + cluster['CacheParameterGroup']['CacheParameterGroupName']), dest) - if self.nested_groups: - self.push_group(self.inventory, 'elasticache_parameter_groups', self.to_safe(cluster['CacheParameterGroup']['CacheParameterGroupName'])) - - # Inventory: Group by replication group - if self.group_by_elasticache_replication_group and 'ReplicationGroupId' in cluster and cluster['ReplicationGroupId']: - self.push(self.inventory, self.to_safe("elasticache_replication_group_" + cluster['ReplicationGroupId']), dest) - if self.nested_groups: - self.push_group(self.inventory, 'elasticache_replication_groups', self.to_safe(cluster['ReplicationGroupId'])) - - # Global Tag: all ElastiCache clusters - self.push(self.inventory, 'elasticache_clusters', cluster['CacheClusterId']) - - host_info = self.get_host_info_dict_from_describe_dict(cluster) - - self.inventory["_meta"]["hostvars"][dest] = host_info - - # Add the nodes - for node in cluster['CacheNodes']: - self.add_elasticache_node(node, cluster, region) - - def add_elasticache_node(self, node, cluster, region): - ''' Adds an ElastiCache node to the inventory and index, as long as - it is addressable ''' - - # Only want available nodes unless all_elasticache_nodes is True - if not self.all_elasticache_nodes and node['CacheNodeStatus'] != 'available': - return - - # Select the best destination address - dest = node['Endpoint']['Address'] - - if not dest: - # Skip nodes we cannot address (e.g. private VPC subnet) - return - - node_id = self.to_safe(cluster['CacheClusterId'] + '_' + node['CacheNodeId']) - - # Add to index - self.index[dest] = [region, node_id] - - # Inventory: Group by node ID (always a group of 1) - if self.group_by_instance_id: - self.inventory[node_id] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'instances', node_id) - - # Inventory: Group by region - if self.group_by_region: - self.push(self.inventory, region, dest) - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - - # Inventory: Group by availability zone - if self.group_by_availability_zone: - self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) - if self.nested_groups: - if self.group_by_region: - self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) - self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) - - # Inventory: Group by node type - if self.group_by_instance_type: - type_name = self.to_safe('type_' + cluster['CacheNodeType']) - self.push(self.inventory, type_name, dest) - if self.nested_groups: - self.push_group(self.inventory, 'types', type_name) - - # Inventory: Group by VPC (information not available in the current - # AWS API version for ElastiCache) - - # Inventory: Group by security group - if self.group_by_security_group: - - # Check for the existence of the 'SecurityGroups' key and also if - # this key has some value. When the cluster is not placed in a SG - # the query can return None here and cause an error. - if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: - for security_group in cluster['SecurityGroups']: - key = self.to_safe("security_group_" + security_group['SecurityGroupId']) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'security_groups', key) - - # Inventory: Group by engine - if self.group_by_elasticache_engine: - self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) - if self.nested_groups: - self.push_group(self.inventory, 'elasticache_engines', self.to_safe("elasticache_" + cluster['Engine'])) - - # Inventory: Group by parameter group (done at cluster level) - - # Inventory: Group by replication group (done at cluster level) - - # Inventory: Group by ElastiCache Cluster - if self.group_by_elasticache_cluster: - self.push(self.inventory, self.to_safe("elasticache_cluster_" + cluster['CacheClusterId']), dest) - - # Global Tag: all ElastiCache nodes - self.push(self.inventory, 'elasticache_nodes', dest) - - host_info = self.get_host_info_dict_from_describe_dict(node) - - if dest in self.inventory["_meta"]["hostvars"]: - self.inventory["_meta"]["hostvars"][dest].update(host_info) - else: - self.inventory["_meta"]["hostvars"][dest] = host_info - - def add_elasticache_replication_group(self, replication_group, region): - ''' Adds an ElastiCache replication group to the inventory and index ''' - - # Only want available clusters unless all_elasticache_replication_groups is True - if not self.all_elasticache_replication_groups and replication_group['Status'] != 'available': - return - - # Select the best destination address (PrimaryEndpoint) - dest = replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address'] - - if not dest: - # Skip clusters we cannot address (e.g. private VPC subnet) - return - - # Add to index - self.index[dest] = [region, replication_group['ReplicationGroupId']] - - # Inventory: Group by ID (always a group of 1) - if self.group_by_instance_id: - self.inventory[replication_group['ReplicationGroupId']] = [dest] - if self.nested_groups: - self.push_group(self.inventory, 'instances', replication_group['ReplicationGroupId']) - - # Inventory: Group by region - if self.group_by_region: - self.push(self.inventory, region, dest) - if self.nested_groups: - self.push_group(self.inventory, 'regions', region) - - # Inventory: Group by availability zone (doesn't apply to replication groups) - - # Inventory: Group by node type (doesn't apply to replication groups) - - # Inventory: Group by VPC (information not available in the current - # AWS API version for replication groups - - # Inventory: Group by security group (doesn't apply to replication groups) - # Check this value in cluster level - - # Inventory: Group by engine (replication groups are always Redis) - if self.group_by_elasticache_engine: - self.push(self.inventory, 'elasticache_redis', dest) - if self.nested_groups: - self.push_group(self.inventory, 'elasticache_engines', 'redis') - - # Global Tag: all ElastiCache clusters - self.push(self.inventory, 'elasticache_replication_groups', replication_group['ReplicationGroupId']) - - host_info = self.get_host_info_dict_from_describe_dict(replication_group) - - self.inventory["_meta"]["hostvars"][dest] = host_info - - def get_route53_records(self): - ''' Get and store the map of resource records to domain names that - point to them. ''' - - r53_conn = route53.Route53Connection() - all_zones = r53_conn.get_zones() - - route53_zones = [ zone for zone in all_zones if zone.name[:-1] - not in self.route53_excluded_zones ] - - self.route53_records = {} - - for zone in route53_zones: - rrsets = r53_conn.get_all_rrsets(zone.id) - - for record_set in rrsets: - record_name = record_set.name - - if record_name.endswith('.'): - record_name = record_name[:-1] - - for resource in record_set.resource_records: - self.route53_records.setdefault(resource, set()) - self.route53_records[resource].add(record_name) - - - def get_instance_route53_names(self, instance): - ''' Check if an instance is referenced in the records we have from - Route53. If it is, return the list of domain names pointing to said - instance. If nothing points to it, return an empty list. ''' - - instance_attributes = [ 'public_dns_name', 'private_dns_name', - 'ip_address', 'private_ip_address' ] - - name_list = set() - - for attrib in instance_attributes: - try: - value = getattr(instance, attrib) - except AttributeError: - continue - - if value in self.route53_records: - name_list.update(self.route53_records[value]) - - return list(name_list) - - def get_host_info_dict_from_instance(self, instance): - instance_vars = {} - for key in vars(instance): - value = getattr(instance, key) - key = self.to_safe('ec2_' + key) - - # Handle complex types - # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518 - if key == 'ec2__state': - instance_vars['ec2_state'] = instance.state or '' - instance_vars['ec2_state_code'] = instance.state_code - elif key == 'ec2__previous_state': - instance_vars['ec2_previous_state'] = instance.previous_state or '' - instance_vars['ec2_previous_state_code'] = instance.previous_state_code - elif type(value) in [int, bool]: - instance_vars[key] = value - elif isinstance(value, six.string_types): - instance_vars[key] = value.strip() - elif type(value) == type(None): - instance_vars[key] = '' - elif key == 'ec2_region': - instance_vars[key] = value.name - elif key == 'ec2__placement': - instance_vars['ec2_placement'] = value.zone - elif key == 'ec2_tags': - for k, v in value.items(): - if self.expand_csv_tags and ',' in v: - v = map(lambda x: x.strip(), v.split(',')) - key = self.to_safe('ec2_tag_' + k) - instance_vars[key] = v - elif key == 'ec2_groups': - group_ids = [] - group_names = [] - for group in value: - group_ids.append(group.id) - group_names.append(group.name) - instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids]) - instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names]) - else: - pass - # TODO Product codes if someone finds them useful - #print key - #print type(value) - #print value - - return instance_vars - - def get_host_info_dict_from_describe_dict(self, describe_dict): - ''' Parses the dictionary returned by the API call into a flat list - of parameters. This method should be used only when 'describe' is - used directly because Boto doesn't provide specific classes. ''' - - # I really don't agree with prefixing everything with 'ec2' - # because EC2, RDS and ElastiCache are different services. - # I'm just following the pattern used until now to not break any - # compatibility. - - host_info = {} - for key in describe_dict: - value = describe_dict[key] - key = self.to_safe('ec2_' + self.uncammelize(key)) - - # Handle complex types - - # Target: Memcached Cache Clusters - if key == 'ec2_configuration_endpoint' and value: - host_info['ec2_configuration_endpoint_address'] = value['Address'] - host_info['ec2_configuration_endpoint_port'] = value['Port'] - - # Target: Cache Nodes and Redis Cache Clusters (single node) - if key == 'ec2_endpoint' and value: - host_info['ec2_endpoint_address'] = value['Address'] - host_info['ec2_endpoint_port'] = value['Port'] - - # Target: Redis Replication Groups - if key == 'ec2_node_groups' and value: - host_info['ec2_endpoint_address'] = value[0]['PrimaryEndpoint']['Address'] - host_info['ec2_endpoint_port'] = value[0]['PrimaryEndpoint']['Port'] - replica_count = 0 - for node in value[0]['NodeGroupMembers']: - if node['CurrentRole'] == 'primary': - host_info['ec2_primary_cluster_address'] = node['ReadEndpoint']['Address'] - host_info['ec2_primary_cluster_port'] = node['ReadEndpoint']['Port'] - host_info['ec2_primary_cluster_id'] = node['CacheClusterId'] - elif node['CurrentRole'] == 'replica': - host_info['ec2_replica_cluster_address_'+ str(replica_count)] = node['ReadEndpoint']['Address'] - host_info['ec2_replica_cluster_port_'+ str(replica_count)] = node['ReadEndpoint']['Port'] - host_info['ec2_replica_cluster_id_'+ str(replica_count)] = node['CacheClusterId'] - replica_count += 1 - - # Target: Redis Replication Groups - if key == 'ec2_member_clusters' and value: - host_info['ec2_member_clusters'] = ','.join([str(i) for i in value]) - - # Target: All Cache Clusters - elif key == 'ec2_cache_parameter_group': - host_info["ec2_cache_node_ids_to_reboot"] = ','.join([str(i) for i in value['CacheNodeIdsToReboot']]) - host_info['ec2_cache_parameter_group_name'] = value['CacheParameterGroupName'] - host_info['ec2_cache_parameter_apply_status'] = value['ParameterApplyStatus'] - - # Target: Almost everything - elif key == 'ec2_security_groups': - - # Skip if SecurityGroups is None - # (it is possible to have the key defined but no value in it). - if value is not None: - sg_ids = [] - for sg in value: - sg_ids.append(sg['SecurityGroupId']) - host_info["ec2_security_group_ids"] = ','.join([str(i) for i in sg_ids]) - - # Target: Everything - # Preserve booleans and integers - elif type(value) in [int, bool]: - host_info[key] = value - - # Target: Everything - # Sanitize string values - elif isinstance(value, six.string_types): - host_info[key] = value.strip() - - # Target: Everything - # Replace None by an empty string - elif type(value) == type(None): - host_info[key] = '' - - else: - # Remove non-processed complex types - pass - - return host_info - - def get_host_info(self): - ''' Get variables about a specific host ''' - - if len(self.index) == 0: - # Need to load index from cache - self.load_index_from_cache() - - if not self.args.host in self.index: - # try updating the cache - self.do_api_calls_update_cache() - if not self.args.host in self.index: - # host might not exist anymore - return self.json_format_dict({}, True) - - (region, instance_id) = self.index[self.args.host] - - instance = self.get_instance(region, instance_id) - return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True) - - def push(self, my_dict, key, element): - ''' Push an element onto an array that may not have been defined in - the dict ''' - group_info = my_dict.setdefault(key, []) - if isinstance(group_info, dict): - host_list = group_info.setdefault('hosts', []) - host_list.append(element) - else: - group_info.append(element) - - def push_group(self, my_dict, key, element): - ''' Push a group as a child of another group. ''' - parent_group = my_dict.setdefault(key, {}) - if not isinstance(parent_group, dict): - parent_group = my_dict[key] = {'hosts': parent_group} - child_groups = parent_group.setdefault('children', []) - if element not in child_groups: - child_groups.append(element) - - def get_inventory_from_cache(self): - ''' Reads the inventory from the cache file and returns it as a JSON - object ''' - - cache = open(self.cache_path_cache, 'r') - json_inventory = cache.read() - return json_inventory - - - def load_index_from_cache(self): - ''' Reads the index from the cache file sets self.index ''' - - cache = open(self.cache_path_index, 'r') - json_index = cache.read() - self.index = json.loads(json_index) - - - def write_to_cache(self, data, filename): - ''' Writes data in JSON format to a file ''' - - json_data = self.json_format_dict(data, True) - cache = open(filename, 'w') - cache.write(json_data) - cache.close() - - def uncammelize(self, key): - temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower() - - def to_safe(self, word): - ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' - regex = "[^A-Za-z0-9\_" - if not self.replace_dash_in_groups: - regex += "\-" - return re.sub(regex + "]", "_", word) - - def json_format_dict(self, data, pretty=False): - ''' Converts a dict to a JSON object and dumps it as a formatted - string ''' - - if pretty: - return json.dumps(data, sort_keys=True, indent=2) - else: - return json.dumps(data) - - -# Run the script -Ec2Inventory() diff --git a/ec2.yml b/ec2.yml index 1aa6c145..031f2112 100644 --- a/ec2.yml +++ b/ec2.yml @@ -2,6 +2,8 @@ - name: Create a sandbox instance hosts: localhost gather_facts: False + vars_files: + - config.cfg vars: instance_type: t2.nano security_group: vpn-secgroup @@ -19,6 +21,15 @@ "11": "sa-east-1" vars_prompt: + + - name: "aws_access_key" + prompt: "Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" + private: yes + + - name: "aws_secret_key" + prompt: "Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" + private: yes + - name: "region" prompt: > What region should the server be located in? @@ -35,6 +46,15 @@ 11. sa-east-1 South America (São Paulo) default: "1" private: no + + - name: "aws_server_name" + prompt: "Name the vpn server:\n" + default: "algo.local" + private: no + + - name: "ssh_public_key" + prompt: "Enter the local path to your SSH public key (ex: ~/.ssh/id_rsa.pub):\n" + private: no - name: "dns_enabled" prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" @@ -46,89 +66,8 @@ default: "Y" private: no - tasks: - - - name: Grab the default interface subnet. - ec2_eni_facts: - region: "{{ regions[region] }}" - register: ec2_enis - - - name: Locate official Ubuntu 16.04 AMI for region. - ec2_ami_find: - name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" - owner: 099720109477 - sort: name - sort_order: descending - sort_end: 1 - region: "{{ regions[region] }}" - register: ami_search - - - set_fact: - ami_image: "{{ ami_search.results[0].ami_id }}" - default_subnet: "{{ ec2_enis.interfaces[0].subnet_id }}" - - - name: Fetch our IP for security group. - ipify_facts: - - - name: Add ssh public key. - ec2_key: - name: VPNKEY - region: "{{ regions[region] }}" - key_material: "{{ item }}" - with_file: ~/.ssh/id_rsa.pub - register: keypair - - - name: Configure EC2 security group - ec2_group: - name: "{{ security_group }}" - description: Security group for VPN servers - region: "{{ regions[region] }}" - rules: - - proto: udp - from_port: 4500 - to_port: 4500 - cidr_ip: 0.0.0.0/0 - - proto: udp - from_port: 500 - to_port: 500 - cidr_ip: 0.0.0.0/0 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: "{{ ipify_public_ip }}/32" - rules_egress: - - proto: all - from_port: 0-65535 - to_port: 0-65535 - cidr_ip: 0.0.0.0/0 - - - name: Launch instance - ec2: - keypair: "VPNKEY" - group: "{{ security_group }}" - instance_type: "{{ instance_type }}" - image: "{{ ami_image }}" - wait: true - region: "{{ regions[region] }}" - vpc_subnet_id: "{{ default_subnet }}" - assign_public_ip: yes - instance_tags: - Name: VPN - register: ec2 - - - name: Add new instance to host group - add_host: - hostname: "{{ item.public_ip }}" - groupname: vpn-host - remote_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - dns_enabled: "{{ dns_enabled }}" - auditd_enabled: " {{ auditd_enabled }}" - with_items: "{{ ec2.instances }}" - - - name: Wait for SSH to come up - wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started - with_items: "{{ ec2.instances }}" + roles: + - ec2 - name: Post-provisioning tasks hosts: vpn-host @@ -147,5 +86,6 @@ - common - security - features - - vpn + - vpn + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/inventory_users b/inventory_users index 3f926363..4c8c1a74 100644 --- a/inventory_users +++ b/inventory_users @@ -1,4 +1,6 @@ [user-management] +52.58.224.125 +52.28.49.194 146.185.162.155 37.139.21.209 37.139.0.99 diff --git a/roles/ec2/handlers/main.yml b/roles/ec2/handlers/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/ec2/tasks/main.yml b/roles/ec2/tasks/main.yml new file mode 100644 index 00000000..e9f57b0f --- /dev/null +++ b/roles/ec2/tasks/main.yml @@ -0,0 +1,78 @@ +- name: Locate official Ubuntu 16.04 AMI for region. + ec2_ami_find: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" + owner: 099720109477 + sort: name + sort_order: descending + sort_end: 1 + region: "{{ regions[region] }}" + register: ami_search + +- set_fact: + ami_image: "{{ ami_search.results[0].ami_id }}" + +- name: Add ssh public key. + ec2_key: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + name: VPNKEY + region: "{{ regions[region] }}" + key_material: "{{ item }}" + with_file: "{{ ssh_public_key }}" + register: keypair + +- name: Configure EC2 security group + ec2_group: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + name: "{{ security_group }}" + description: Security group for VPN servers + region: "{{ regions[region] }}" + rules: + - proto: udp + from_port: 4500 + to_port: 4500 + cidr_ip: 0.0.0.0/0 + - proto: udp + from_port: 500 + to_port: 500 + cidr_ip: 0.0.0.0/0 + - proto: tcp + from_port: 22 + to_port: 22 + cidr_ip: 0.0.0.0/0 + rules_egress: + - proto: all + from_port: 0-65535 + to_port: 0-65535 + cidr_ip: 0.0.0.0/0 + +- name: Launch instance + ec2: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + keypair: "VPNKEY" + group: "{{ security_group }}" + instance_type: "{{ instance_type }}" + image: "{{ ami_image }}" + wait: true + region: "{{ regions[region] }}" + instance_tags: + name: "{{ aws_server_name }}" + register: ec2 + +- name: Add new instance to host group + add_host: + hostname: "{{ item.public_ip }}" + groupname: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + dns_enabled: "{{ dns_enabled }}" + auditd_enabled: " {{ auditd_enabled }}" + with_items: "{{ ec2.instances }}" + +- name: Wait for SSH to come up + wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started + with_items: "{{ ec2.instances }}" diff --git a/roles/logging/handlers/main.yml b/roles/logging/handlers/main.yml new file mode 100644 index 00000000..651d8a7d --- /dev/null +++ b/roles/logging/handlers/main.yml @@ -0,0 +1,2 @@ +- name: restart auditd + service: name=auditd state=restarted diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml new file mode 100644 index 00000000..e6a88854 --- /dev/null +++ b/roles/logging/tasks/main.yml @@ -0,0 +1,16 @@ +- name: Auditd installed + apt: name=auditd state=latest + +- name: Auditd rules configured + template: src=audit.rules.j2 dest=/etc/audit/audit.rules + notify: + - restart auditd + +- name: Auditd configured + template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf + notify: + - restart auditd + +- name: Enable services + service: name=auditd enabled=yes + diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index 248066a9..dd2210b6 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,6 +1,3 @@ -- name: restart auditd - service: name=auditd state=restarted - - name: restart rsyslog service: name=rsyslog state=restarted diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index cfe41c5a..e7fa93e8 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,5 +1,3 @@ - - # Using a two-pass approach for checking directories in order to support symlinks. - name: Find directories for minimizing access stat: @@ -32,28 +30,6 @@ executable: /bin/bash register: privileged_programs -# auditd - -- name: Auditd installed - apt: name=auditd state=latest - when: auditd_enabled is defined and auditd_enabled == 'Y' - -- name: Auditd rules configured - template: src=audit.rules.j2 dest=/etc/audit/audit.rules - notify: - - restart auditd - when: auditd_enabled is defined and auditd_enabled == 'Y' - -- name: Auditd configured - template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf - notify: - - restart auditd - when: auditd_enabled is defined and auditd_enabled == 'Y' - -- name: Enable services - service: name=auditd enabled=yes - when: auditd_enabled is defined and auditd_enabled == 'Y' - # Rsyslog - name: Rsyslog configured From d960a2cc21e0194a76b5aac8884da3d5197efa9c Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 22:38:50 +0300 Subject: [PATCH 013/769] Readme --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 6344c870..16c3549f 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,6 @@ There are two available cloud providers: * DigitalOcean * Amazon EC2 -If you want to use Amazon EC2, ensure that you setup the required environment variables prior to starting the deploy: -``` -declare -x AWS_ACCESS_KEY_ID="XXXXXXXXXXXXXXXXXXX" -declare -x AWS_SECRET_ACCESS_KEY="XXXXXXXXXXXXXXXxx" -``` - Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. Start the deploy and follow the instructions: From fa6e3db0023a163daedab29f193f4f015349c651 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 22:41:40 +0300 Subject: [PATCH 014/769] clean up --- ec2-destroy.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 ec2-destroy.yml diff --git a/ec2-destroy.yml b/ec2-destroy.yml deleted file mode 100644 index 95146f6e..00000000 --- a/ec2-destroy.yml +++ /dev/null @@ -1,14 +0,0 @@ -- name: Create a sandbox instance - hosts: localhost - gather_facts: False - vars_files: - - config.cfg - - tasks: - - name: Terminate instances that were previously launched - ec2: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - region: eu-central-1 - state: 'absent' - instance_ids: "{{ id }}" From 2078d952ae190fc768f19539f86403e60698983e Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 22:43:14 +0300 Subject: [PATCH 015/769] clean up --- inventory_users | 5 ----- 1 file changed, 5 deletions(-) diff --git a/inventory_users b/inventory_users index 4c8c1a74..cafed486 100644 --- a/inventory_users +++ b/inventory_users @@ -1,6 +1 @@ [user-management] -52.58.224.125 -52.28.49.194 -146.185.162.155 -37.139.21.209 -37.139.0.99 From f6c1309aac301c6062c6ad3fa1586d6522f09345 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 23:40:07 +0300 Subject: [PATCH 016/769] non-cloud servers #34 --- digitalocean.yml | 1 - ec2.yml | 2 +- inventory_users | 1 + non-cloud.yml | 59 ++++++++++++++++++++++++++++++++++++++++ roles/ec2/tasks/main.yml | 6 ++-- run | 3 ++ 6 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 non-cloud.yml diff --git a/digitalocean.yml b/digitalocean.yml index 51bf1f20..c83f9612 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -108,7 +108,6 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" - become: false roles: - common diff --git a/ec2.yml b/ec2.yml index 031f2112..2e1bdfd7 100644 --- a/ec2.yml +++ b/ec2.yml @@ -80,7 +80,7 @@ - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 roles: - common diff --git a/inventory_users b/inventory_users index cafed486..1c4194e4 100644 --- a/inventory_users +++ b/inventory_users @@ -1 +1,2 @@ [user-management] +52.59.88.212 diff --git a/non-cloud.yml b/non-cloud.yml new file mode 100644 index 00000000..be31f0a4 --- /dev/null +++ b/non-cloud.yml @@ -0,0 +1,59 @@ +- hosts: localhost + gather_facts: False + vars_files: + - config.cfg + vars_prompt: + + - name: "server_ip" + prompt: "Enter IP address of your server:\n" + private: no + + - name: "server_user" + prompt: "What user should we use?:\n" + default: "root" + private: no + + - name: "dns_enabled" + prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no + + - name: "auditd_enabled" + prompt: "Do you want to use auditd ? (Y or N):\n" + default: "Y" + private: no + + tasks: + - name: Add the server to the vpn-host group + add_host: + hostname: "{{ server_ip }}" + groupname: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + dns_enabled: "{{ dns_enabled }}" + auditd_enabled: " {{ auditd_enabled }}" + + - name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ server_ip }} timeout=320" + become: false + +- name: Post-provisioning tasks + hosts: vpn-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Install prerequisites + raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + - name: Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + + roles: + - common + - security + - features + - vpn + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + diff --git a/roles/ec2/tasks/main.yml b/roles/ec2/tasks/main.yml index e9f57b0f..52a5fac8 100644 --- a/roles/ec2/tasks/main.yml +++ b/roles/ec2/tasks/main.yml @@ -73,6 +73,8 @@ auditd_enabled: " {{ auditd_enabled }}" with_items: "{{ ec2.instances }}" -- name: Wait for SSH to come up - wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started +- name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ item.public_dns_name }} timeout=320" with_items: "{{ ec2.instances }}" + become: false + diff --git a/run b/run index 55419f0e..00482d49 100755 --- a/run +++ b/run @@ -4,6 +4,8 @@ echo -n " What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 + 3. Local installation (non-cloud or a server already deployed) + Enter the number of your desired provider : " @@ -12,6 +14,7 @@ read N case "$N" in 1) CLOUD="digitalocean" ;; 2) CLOUD="ec2" ;; + 3) CLOUD="non-cloud" ;; *) exit 1 ;; esac From 917b7d6138ac91ba4a05d75d9ecbe45893d3ec41 Mon Sep 17 00:00:00 2001 From: jack Date: Thu, 11 Aug 2016 23:54:29 +0300 Subject: [PATCH 017/769] Modify user-management function --- README.md | 12 ++++------- digitalocean.yml | 1 + inventory_users | 2 -- non-cloud.yml | 2 +- roles/vpn/tasks/main.yml | 4 ---- run | 46 ++++++++++++++++++++++++++-------------- users.yml | 29 ++++++++++++++++++++++++- 7 files changed, 64 insertions(+), 32 deletions(-) delete mode 100644 inventory_users diff --git a/README.md b/README.md index 16c3549f..c1b1f980 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,12 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere ### Initial Deployment -To install the dependencies on OS X: +To install the dependencies on OS X or Linux: ``` sudo easy_install pip sudo pip install ansible dopy==0.3.5 +sudo pip install boto ``` There are two available cloud providers: @@ -59,17 +60,12 @@ When the process is done, you can find `.mobileconfig` files and certificates in ### User Management -When the deploy proccess is done a new server will be placed in the local inventory file `inventory_users`. If you want to add or delete users, update the `users` list in `config.cfg` and run the playbook `users.yml`. This command will update users on any servers in the file `inventory_users`. +If you want to add or delete users, update the `users` list in `config.cfg` and run the command: ``` -ansible-playbook users.yml --user=root -i inventory_users +./run users ``` -Note: For EC2 users, Algo does NOT use EC2 dynamic inventory for user management. Please continue to use users.yml playbook as described below. This may be subject to change in the future. - -``` -ansible-playbook users.yml --user=ubuntu -i inventory_users -``` ## FAQ diff --git a/digitalocean.yml b/digitalocean.yml index c83f9612..51bf1f20 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -108,6 +108,7 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" + become: false roles: - common diff --git a/inventory_users b/inventory_users deleted file mode 100644 index 1c4194e4..00000000 --- a/inventory_users +++ /dev/null @@ -1,2 +0,0 @@ -[user-management] -52.59.88.212 diff --git a/non-cloud.yml b/non-cloud.yml index be31f0a4..19a9c77a 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -9,7 +9,7 @@ private: no - name: "server_user" - prompt: "What user should we use?:\n" + prompt: "What user should we use to login on the server?:\n" default: "root" private: no diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 478c4370..c1bf4f8f 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -136,9 +136,5 @@ - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes - -- name: Add server to the inventory file - local_action: lineinfile dest=inventory_users line="{{ inventory_hostname }}" insertafter='\[user-management\]\n' state=present - become: false notify: - congrats diff --git a/run b/run index 00482d49..52837177 100755 --- a/run +++ b/run @@ -1,21 +1,35 @@ #!/bin/sh -echo -n " -What provider would you like to use? - 1. DigitalOcean - 2. Amazon EC2 - 3. Local installation (non-cloud or a server already deployed) - -Enter the number of your desired provider -: " +algo_provisioning () { + echo -n " + What provider would you like to use? + 1. DigitalOcean + 2. Amazon EC2 + 3. Local installation (non-cloud or a server already deployed) + + Enter the number of your desired provider + : " + + read N + + case "$N" in + 1) CLOUD="digitalocean" ;; + 2) CLOUD="ec2" ;; + 3) CLOUD="non-cloud" ;; + *) exit 1 ;; + esac + + ansible-playbook deploy.yml -e "provider=${CLOUD}" +} + +user_management () { + ansible-playbook users.yml +} + +case "$1" in + users) user_management ;; + *) algo_provisioning ;; +esac -read N -case "$N" in - 1) CLOUD="digitalocean" ;; - 2) CLOUD="ec2" ;; - 3) CLOUD="non-cloud" ;; - *) exit 1 ;; -esac -ansible-playbook deploy.yml -e "provider=${CLOUD}" diff --git a/users.yml b/users.yml index 893a5505..f995cd45 100644 --- a/users.yml +++ b/users.yml @@ -1,7 +1,34 @@ --- +- hosts: localhost + gather_facts: False + vars_files: + - config.cfg + vars_prompt: + + - name: "server_ip" + prompt: "\nEnter IP address of your server:\n" + private: no + + - name: "server_user" + prompt: "What user should we use to login on the server?:\n" + default: "root" + private: no + + tasks: + - name: Add the server to the vpn-host group + add_host: + hostname: "{{ server_ip }}" + groupname: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + + - name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ server_ip }} timeout=320" + become: false + - name: User management - hosts: user-management + hosts: vpn-host gather_facts: false become: true vars_files: From 4f3e3230e3acebe27f2d848e3a771fc670d0ae3e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 11 Aug 2016 17:25:54 -0400 Subject: [PATCH 018/769] rename a few things --- README.md | 15 +++++++-------- run => algo | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) rename run => algo (93%) diff --git a/README.md b/README.md index c1b1f980..1c891fa7 100644 --- a/README.md +++ b/README.md @@ -39,31 +39,30 @@ To install the dependencies on OS X or Linux: ``` sudo easy_install pip -sudo pip install ansible dopy==0.3.5 -sudo pip install boto +sudo pip install ansible dopy==0.3.5 boto ``` -There are two available cloud providers: +There are three available installation targets: * DigitalOcean * Amazon EC2 +* Local servers -Open the file `config.cfg` in your favorite text editor. Specify users in the `users` list. +Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. Start the deploy and follow the instructions: ``` -./run +./algo ``` -When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext since it contains the keys to access the VPN. For those using other clients, like Windows or Android, send the X.509 certificates for the server and their user. - +When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. ### User Management If you want to add or delete users, update the `users` list in `config.cfg` and run the command: ``` -./run users +./algo update-users ``` diff --git a/run b/algo similarity index 93% rename from run rename to algo index 52837177..781ee17c 100755 --- a/run +++ b/algo @@ -27,7 +27,7 @@ user_management () { } case "$1" in - users) user_management ;; + update-users) user_management ;; *) algo_provisioning ;; esac From f0d31719e0ef4fa119e6473ae7e0bf0bf474e7bf Mon Sep 17 00:00:00 2001 From: Jay Little Date: Fri, 12 Aug 2016 19:05:49 -0400 Subject: [PATCH 019/769] Add depends info for installing into an existing system --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 1c891fa7..f7424c27 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,13 @@ sudo easy_install pip sudo pip install ansible dopy==0.3.5 boto ``` +To install the dependencies for installing on an existing Ubuntu 16.04 system: + +``` +sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible +sudo apt-get update && sudo apt-get install ansible +``` + There are three available installation targets: * DigitalOcean * Amazon EC2 From f0366562aa344544b85e68b7ac7d0dac434ca945 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 14:10:57 +0300 Subject: [PATCH 020/769] google and azure --- README.md | 3 +++ config.cfg | 3 --- run | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c1b1f980..c3cc5f02 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * python >= 2.6 * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) * [boto](https://github.com/boto/boto) +* [azure >= 0.7.1](https://github.com/Azure/azure-sdk-for-python) +* [apache-libcloud](https://github.com/apache/libcloud) +* [libcloud](https://curl.haxx.se/docs/caextract.html) (for Mac OS) * SHell or BASH * libselinux-python (for RedHat based distros) diff --git a/config.cfg b/config.cfg index bfd3aa2f..c17bbf47 100644 --- a/config.cfg +++ b/config.cfg @@ -30,6 +30,3 @@ users: # auditd options # email for auditd actions: auditd_action_mail_acct: email@example.com - - - diff --git a/run b/run index 52837177..bf754c61 100755 --- a/run +++ b/run @@ -5,7 +5,7 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 - 3. Local installation (non-cloud or a server already deployed) + 0. Local installation (non-cloud or a server already deployed) Enter the number of your desired provider : " @@ -15,7 +15,9 @@ algo_provisioning () { case "$N" in 1) CLOUD="digitalocean" ;; 2) CLOUD="ec2" ;; - 3) CLOUD="non-cloud" ;; + 3) CLOUD="azure" ;; + 4) CLOUD="google_cloud" ;; + 0) CLOUD="non-cloud" ;; *) exit 1 ;; esac From 97865f40ec5442fabcb56859cecabcb3361f9e01 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 14:11:48 +0300 Subject: [PATCH 021/769] google and azure --- run | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run b/run index bf754c61..6041d908 100755 --- a/run +++ b/run @@ -5,6 +5,8 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 + 3. Azure + 4. Google-cloud 0. Local installation (non-cloud or a server already deployed) Enter the number of your desired provider From 3870956f0a8dc7f37ffa16d9907a528814c0eab4 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 14:13:23 +0300 Subject: [PATCH 022/769] google and azure --- azure.yml | 99 ++++++++++++++++++++++++++++ google_cloud.yml | 99 ++++++++++++++++++++++++++++ roles/azure/handlers/main.yml | 0 roles/azure/tasks/main.yml | 45 +++++++++++++ roles/google_cloud/handlers/main.yml | 0 roles/google_cloud/tasks/main.yml | 13 ++++ 6 files changed, 256 insertions(+) create mode 100644 azure.yml create mode 100644 google_cloud.yml create mode 100644 roles/azure/handlers/main.yml create mode 100644 roles/azure/tasks/main.yml create mode 100644 roles/google_cloud/handlers/main.yml create mode 100644 roles/google_cloud/tasks/main.yml diff --git a/azure.yml b/azure.yml new file mode 100644 index 00000000..5e35b77d --- /dev/null +++ b/azure.yml @@ -0,0 +1,99 @@ +- name: Configure the server and install required software + hosts: localhost + gather_facts: false + + vars: + regions: + "1": "East US" + "2": "West US" + "3": "South Central US" + "4": "North Europe" + "5": "East Asia" + "6": "Japan East" + "7": "West Europe" + "8": "Southeast Asia" + "9": "Japan West" + "10": "North Central US" + "11": "Central US" + "12": "Brazil South" + "13": "East US 2" + "14": "Australia Southeast" + "15": "Australia East" + + #vars_prompt: + #- name: "azure_subscription_id" + #prompt: "Enter your subscription ID (https://blogs.msdn.microsoft.com/mschray/2015/05/13/getting-your-azure-guid-subscription-id/):\n" + #private: yes + + #- name: "management_cert_path" + #prompt: "Enter the local path to your management cert [ex: ~/.ssh/id_rsa.pub] (https://azure.microsoft.com/en-us/documentation/articles/azure-api-management-certs/):\n" + #private: no + + #- name: "ssh_public_key" + #prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" + #private: no + + #- name: "region" + #prompt: > + #What region should the server be located in? + #1. East US + #2. West US + #3. South Central US + #4. North Europe + #5. East Asia + #6. Japan East + #7. West Europe + #8. Southeast Asia + #9. Japan West + #10. North Central US + #11. Central US + #12. Brazil South + #13. East US 2 + #14. Australia Southeast + #15. Australia East + #Enter the number of your desired region: + #default: "7" + #private: no + + #- name: "azure_server_name" + #prompt: "Name the vpn server:\n" + #default: "algo.local" + #private: no + + #- name: "dns_enabled" + #prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + #default: "Y" + #private: no + + #- name: "auditd_enabled" + #prompt: "Do you want to use auditd ? (Y or N):\n" + #default: "Y" + #private: no + + roles: + - azure + +- name: Post-provisioning tasks + hosts: vpn-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Install prerequisites + raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + - name: Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + + roles: + - common + - security + - features + - vpn + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + + + + + diff --git a/google_cloud.yml b/google_cloud.yml new file mode 100644 index 00000000..80da93b0 --- /dev/null +++ b/google_cloud.yml @@ -0,0 +1,99 @@ +- name: Configure the server and install required software + hosts: localhost + gather_facts: false + + vars: + regions: + "1": "East US" + "2": "West US" + "3": "South Central US" + "4": "North Europe" + "5": "East Asia" + "6": "Japan East" + "7": "West Europe" + "8": "Southeast Asia" + "9": "Japan West" + "10": "North Central US" + "11": "Central US" + "12": "Brazil South" + "13": "East US 2" + "14": "Australia Southeast" + "15": "Australia East" + + #vars_prompt: + #- name: "azure_subscription_id" + #prompt: "Enter your subscription ID (https://blogs.msdn.microsoft.com/mschray/2015/05/13/getting-your-azure-guid-subscription-id/):\n" + #private: yes + + #- name: "management_cert_path" + #prompt: "Enter the local path to your management cert [ex: ~/.ssh/id_rsa.pub] (https://azure.microsoft.com/en-us/documentation/articles/azure-api-management-certs/):\n" + #private: no + + #- name: "ssh_public_key" + #prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" + #private: no + + #- name: "region" + #prompt: > + #What region should the server be located in? + #1. East US + #2. West US + #3. South Central US + #4. North Europe + #5. East Asia + #6. Japan East + #7. West Europe + #8. Southeast Asia + #9. Japan West + #10. North Central US + #11. Central US + #12. Brazil South + #13. East US 2 + #14. Australia Southeast + #15. Australia East + #Enter the number of your desired region: + #default: "7" + #private: no + + #- name: "azure_server_name" + #prompt: "Name the vpn server:\n" + #default: "algo.local" + #private: no + + #- name: "dns_enabled" + #prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + #default: "Y" + #private: no + + #- name: "auditd_enabled" + #prompt: "Do you want to use auditd ? (Y or N):\n" + #default: "Y" + #private: no + + roles: + - google_cloud + +- name: Post-provisioning tasks + hosts: vpn-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Install prerequisites + raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + - name: Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + + roles: + - common + - security + - features + - vpn + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + + + + + diff --git a/roles/azure/handlers/main.yml b/roles/azure/handlers/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/azure/tasks/main.yml b/roles/azure/tasks/main.yml new file mode 100644 index 00000000..b17811c8 --- /dev/null +++ b/roles/azure/tasks/main.yml @@ -0,0 +1,45 @@ +- local_action: + module: azure + name: my-virtual-machine + role_size: Small + image: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu_DAILY_BUILD-precise-12_04_3-LTS-amd64-server-20131205-en-us-30GB + location: 'East US' + user: ubuntu + ssh_cert_path: "/home/jack/.ssh/upwork.pub" + storage_account: my-storage-account + wait: yes + subscription_id: "02e68d20-1a39-4faa-aa35-6bdd0238b54e" + management_cert_path: "/home/jack/ownCloud/Clouds/azure/manage.cer" + + +#- name: "Creating a virtual machine..." + #azure: + #subscription_id: "02e68d20-1a39-4faa-aa35-6bdd0238b54e" + #name: "algo-vpn" + #role_size: Small + #image: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-16_04-LTS-amd64-server-20160721-en-us-30GB + #location: "West Europe" + #user: ubuntu + #ssh_cert_path: "/home/jack/.ssh/upwork.pub" + #storage_account: 'algo.vpn' + #management_cert_path: "/home/jack/ownCloud/Clouds/azure/manage.cer" + #wait: yes + #state: present + #register: azure_vm + +- debug: msg="{{ azure_vm }}" + +#- name: Add the droplet to an inventory group + #add_host: + #name: "{{ do.droplet.ip_address }}" + #groups: vpn-host + #ansible_ssh_user: root + #ansible_python_interpreter: "/usr/bin/python2.7" + #do_access_token: "{{ do_access_token }}" + #do_droplet_id: "{{ do.droplet.id }}" + #dns_enabled: "{{ dns_enabled }}" + #auditd_enabled: " {{ auditd_enabled }}" + +#- name: Wait for SSH to become available + #local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" + diff --git a/roles/google_cloud/handlers/main.yml b/roles/google_cloud/handlers/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/google_cloud/tasks/main.yml b/roles/google_cloud/tasks/main.yml new file mode 100644 index 00000000..ed3b6f39 --- /dev/null +++ b/roles/google_cloud/tasks/main.yml @@ -0,0 +1,13 @@ +- name: Launch instances + gce: + instance_names: dev + zone: us-central1-b + machine_type: n1-standard-1 + image: debian-7-wheezy + service_account_email: e601809@gmail.com + credentials_file: '/home/jack/ownCloud/Clouds/Google/My First Project-72e386228f5e.json' + project_id: algo-833@storied-bearing-140310.iam.gserviceaccount.com + register: google_vm + +- debug: msg="{{ google_vm }}" + From 89758aaec97653b1e7d813f8bc6ec315c7478a77 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 16:36:50 +0300 Subject: [PATCH 023/769] Google Cloud Engine #27 --- google_cloud.yml | 114 ++++++++++++++---------------- roles/google_cloud/tasks/main.yml | 32 ++++++--- run | 4 +- 3 files changed, 79 insertions(+), 71 deletions(-) diff --git a/google_cloud.yml b/google_cloud.yml index 80da93b0..504f82f9 100644 --- a/google_cloud.yml +++ b/google_cloud.yml @@ -3,72 +3,64 @@ gather_facts: false vars: - regions: - "1": "East US" - "2": "West US" - "3": "South Central US" - "4": "North Europe" - "5": "East Asia" - "6": "Japan East" - "7": "West Europe" - "8": "Southeast Asia" - "9": "Japan West" - "10": "North Central US" - "11": "Central US" - "12": "Brazil South" - "13": "East US 2" - "14": "Australia Southeast" - "15": "Australia East" + zones: + "1": "us-central1-a" + "2": "us-central1-b" + "3": "us-central1-c" + "4": "us-central1-f" + "5": "us-east1-b" + "6": "us-east1-c" + "7": "us-east1-d" + "8": "europe-west1-b" + "9": "europe-west1-c" + "10": "europe-west1-d" + "11": "asia-east1-a" + "12": "asia-east1-b" + "13": "asia-east1-c" - #vars_prompt: - #- name: "azure_subscription_id" - #prompt: "Enter your subscription ID (https://blogs.msdn.microsoft.com/mschray/2015/05/13/getting-your-azure-guid-subscription-id/):\n" - #private: yes + vars_prompt: + - name: "credentials_file" + prompt: "Enter the local path to your credentials JSON file [ex: ~/gogle_cloud.json] (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts):\n" + private: no + + - name: "ssh_public_key" + prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" + private: no - #- name: "management_cert_path" - #prompt: "Enter the local path to your management cert [ex: ~/.ssh/id_rsa.pub] (https://azure.microsoft.com/en-us/documentation/articles/azure-api-management-certs/):\n" - #private: no + - name: "zone" + prompt: > + What zone should the server be located in? + 1. Central US (Iowa A) + 2. Central US (Iowa B) + 3. Central US (Iowa C) + 4. Central US (Iowa F) + 5. Eastern US (South Carolina B) + 6. Eastern US (South Carolina C) + 7. Eastern US (South Carolina D) + 8. Western Europe (Belgium B) + 9. Western Europe (Belgium C) + 10. Western Europe (Belgium D) + 11. East Asia (Taiwan A) + 12. East Asia (Taiwan B) + 13. East Asia (Taiwan C) + Please choose the number of your zone. Press enter for default (#8) zone. + default: "8" + private: no - #- name: "ssh_public_key" - #prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" - #private: no - - #- name: "region" - #prompt: > - #What region should the server be located in? - #1. East US - #2. West US - #3. South Central US - #4. North Europe - #5. East Asia - #6. Japan East - #7. West Europe - #8. Southeast Asia - #9. Japan West - #10. North Central US - #11. Central US - #12. Brazil South - #13. East US 2 - #14. Australia Southeast - #15. Australia East - #Enter the number of your desired region: - #default: "7" - #private: no + - name: "server_name" + prompt: "Name the vpn server:\n" + default: "algo" + private: no - #- name: "azure_server_name" - #prompt: "Name the vpn server:\n" - #default: "algo.local" - #private: no + - name: "dns_enabled" + prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no - #- name: "dns_enabled" - #prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" - #default: "Y" - #private: no - - #- name: "auditd_enabled" - #prompt: "Do you want to use auditd ? (Y or N):\n" - #default: "Y" - #private: no + - name: "auditd_enabled" + prompt: "Do you want to use auditd ? (Y or N):\n" + default: "Y" + private: no roles: - google_cloud diff --git a/roles/google_cloud/tasks/main.yml b/roles/google_cloud/tasks/main.yml index ed3b6f39..34ec7135 100644 --- a/roles/google_cloud/tasks/main.yml +++ b/roles/google_cloud/tasks/main.yml @@ -1,13 +1,29 @@ -- name: Launch instances +- set_fact: + credentials_file_lookup: "{{ lookup('file', '{{ credentials_file }}') }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ ssh_public_key }}') }}" + +- name: "Creating a droplet..." gce: - instance_names: dev - zone: us-central1-b + instance_names: "{{ server_name }}" + zone: "{{ zones[zone] }}" machine_type: n1-standard-1 - image: debian-7-wheezy - service_account_email: e601809@gmail.com - credentials_file: '/home/jack/ownCloud/Clouds/Google/My First Project-72e386228f5e.json' - project_id: algo-833@storied-bearing-140310.iam.gserviceaccount.com + image: ubuntu-1604 + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ credentials_file_lookup.project_id }}" + metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' register: google_vm -- debug: msg="{{ google_vm }}" +- name: Add the droplet to an inventory group + add_host: + name: "{{ google_vm.instance_data[0].public_ip}}" + groups: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + dns_enabled: "{{ dns_enabled }}" + auditd_enabled: " {{ auditd_enabled }}" + +- name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ google_vm.instance_data[0].public_ip }} timeout=320" + diff --git a/run b/run index 6041d908..187e5989 100755 --- a/run +++ b/run @@ -9,8 +9,8 @@ algo_provisioning () { 4. Google-cloud 0. Local installation (non-cloud or a server already deployed) - Enter the number of your desired provider - : " +Enter the number of your desired provider +: " read N From 42e6067e4dd2f91ce01dc0df220e9c101a05085f Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 16:51:24 +0300 Subject: [PATCH 024/769] Firewall | Google Cloud Engine #27 --- roles/google_cloud/tasks/main.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/roles/google_cloud/tasks/main.yml b/roles/google_cloud/tasks/main.yml index 34ec7135..dbe5c6c6 100644 --- a/roles/google_cloud/tasks/main.yml +++ b/roles/google_cloud/tasks/main.yml @@ -10,7 +10,7 @@ image: ubuntu-1604 service_account_email: "{{ credentials_file_lookup.client_email }}" credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + project_id: "{{ credentials_file_lookup.project_id }}" metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' register: google_vm @@ -22,8 +22,22 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + +- name: Firewall configured + local_action: + module: gce_net + name: "{{ google_vm.instance_data[0].network }}" + fwname: "algo-ikev2" + allowed: "udp:500,4500;tcp:22" + state: "present" + src_range: 0.0.0.0/0 + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ credentials_file_lookup.project_id }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ google_vm.instance_data[0].public_ip }} timeout=320" + + From e729f0d303eaa1c9b6a786056c62a1fcb5322337 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 20:03:23 +0300 Subject: [PATCH 025/769] Roles and Google cloud --- README.md | 1 + digitalocean.yml | 2 +- roles/azure/tasks/main.yml | 44 - roles/features/tasks/main.yml | 2 +- run | 6 +- templates/000-default.conf.j2 | 11 - templates/10-loopback-services.cfg.j2 | 9 - templates/10periodic.j2 | 4 - templates/20-ipv6.cfg.j2 | 6 - templates/50unattended-upgrades.j2 | 59 - templates/CIS.conf.j2 | 15 - templates/adblock.sh | 50 - templates/audit.rules.j2 | 101 -- templates/auditd.conf.j2 | 32 - templates/dnsmasq.conf.j2 | 669 -------- templates/easy-rsa.vars.j2 | 198 --- templates/ipsec.conf.j2 | 34 - templates/ipsec.secrets.j2 | 2 - templates/mobileconfig.j2 | 144 -- templates/pagespeed.conf.j2 | 369 ----- templates/ports.conf.j2 | 13 - templates/privoxy_config.j2 | 2107 ------------------------- templates/rsyslog.conf.j2 | 61 - templates/usr.sbin.dnsmasq.j2 | 68 - templates/usr.sbin.privoxy.j2 | 15 - users.yml | 2 +- 26 files changed, 6 insertions(+), 4018 deletions(-) delete mode 100644 templates/000-default.conf.j2 delete mode 100644 templates/10-loopback-services.cfg.j2 delete mode 100644 templates/10periodic.j2 delete mode 100644 templates/20-ipv6.cfg.j2 delete mode 100644 templates/50unattended-upgrades.j2 delete mode 100644 templates/CIS.conf.j2 delete mode 100644 templates/adblock.sh delete mode 100644 templates/audit.rules.j2 delete mode 100644 templates/auditd.conf.j2 delete mode 100644 templates/dnsmasq.conf.j2 delete mode 100644 templates/easy-rsa.vars.j2 delete mode 100644 templates/ipsec.conf.j2 delete mode 100644 templates/ipsec.secrets.j2 delete mode 100644 templates/mobileconfig.j2 delete mode 100644 templates/pagespeed.conf.j2 delete mode 100644 templates/ports.conf.j2 delete mode 100644 templates/privoxy_config.j2 delete mode 100644 templates/rsyslog.conf.j2 delete mode 100644 templates/usr.sbin.dnsmasq.j2 delete mode 100644 templates/usr.sbin.privoxy.j2 diff --git a/README.md b/README.md index c3cc5f02..68291315 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * [azure >= 0.7.1](https://github.com/Azure/azure-sdk-for-python) * [apache-libcloud](https://github.com/apache/libcloud) * [libcloud](https://curl.haxx.se/docs/caextract.html) (for Mac OS) +* [six](https://github.com/JioCloud/python-six) * SHell or BASH * libselinux-python (for RedHat based distros) diff --git a/digitalocean.yml b/digitalocean.yml index 51bf1f20..ecd6262f 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -94,7 +94,7 @@ register: droplet_info - name: IPv6 configured - template: src=20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 + template: src=roles/digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 with_items: "{{ droplet_info.json.droplet.networks.v6 }}" notify: - reload eth0 diff --git a/roles/azure/tasks/main.yml b/roles/azure/tasks/main.yml index b17811c8..8b137891 100644 --- a/roles/azure/tasks/main.yml +++ b/roles/azure/tasks/main.yml @@ -1,45 +1 @@ -- local_action: - module: azure - name: my-virtual-machine - role_size: Small - image: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu_DAILY_BUILD-precise-12_04_3-LTS-amd64-server-20131205-en-us-30GB - location: 'East US' - user: ubuntu - ssh_cert_path: "/home/jack/.ssh/upwork.pub" - storage_account: my-storage-account - wait: yes - subscription_id: "02e68d20-1a39-4faa-aa35-6bdd0238b54e" - management_cert_path: "/home/jack/ownCloud/Clouds/azure/manage.cer" - - -#- name: "Creating a virtual machine..." - #azure: - #subscription_id: "02e68d20-1a39-4faa-aa35-6bdd0238b54e" - #name: "algo-vpn" - #role_size: Small - #image: b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-16_04-LTS-amd64-server-20160721-en-us-30GB - #location: "West Europe" - #user: ubuntu - #ssh_cert_path: "/home/jack/.ssh/upwork.pub" - #storage_account: 'algo.vpn' - #management_cert_path: "/home/jack/ownCloud/Clouds/azure/manage.cer" - #wait: yes - #state: present - #register: azure_vm - -- debug: msg="{{ azure_vm }}" - -#- name: Add the droplet to an inventory group - #add_host: - #name: "{{ do.droplet.ip_address }}" - #groups: vpn-host - #ansible_ssh_user: root - #ansible_python_interpreter: "/usr/bin/python2.7" - #do_access_token: "{{ do_access_token }}" - #do_droplet_id: "{{ do.droplet.id }}" - #dns_enabled: "{{ dns_enabled }}" - #auditd_enabled: " {{ auditd_enabled }}" - -#- name: Wait for SSH to become available - #local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" diff --git a/roles/features/tasks/main.yml b/roles/features/tasks/main.yml index b305b80f..8045981a 100644 --- a/roles/features/tasks/main.yml +++ b/roles/features/tasks/main.yml @@ -93,7 +93,7 @@ - restart dnsmasq - name: Adblock script created - copy: src=templates/adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 + template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 when: dns_enabled is defined and dns_enabled == "Y" - name: Adblock script added to cron diff --git a/run b/run index 187e5989..f4cf9ada 100755 --- a/run +++ b/run @@ -5,8 +5,7 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 - 3. Azure - 4. Google-cloud + 3. Google-cloud 0. Local installation (non-cloud or a server already deployed) Enter the number of your desired provider @@ -17,8 +16,7 @@ Enter the number of your desired provider case "$N" in 1) CLOUD="digitalocean" ;; 2) CLOUD="ec2" ;; - 3) CLOUD="azure" ;; - 4) CLOUD="google_cloud" ;; + 3) CLOUD="google_cloud" ;; 0) CLOUD="non-cloud" ;; *) exit 1 ;; esac diff --git a/templates/000-default.conf.j2 b/templates/000-default.conf.j2 deleted file mode 100644 index 7aa917b7..00000000 --- a/templates/000-default.conf.j2 +++ /dev/null @@ -1,11 +0,0 @@ - - - Order deny,allow - Allow from all - - RewriteEngine On - RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] - ProxyPass / http://$1 - ProxyPassReverse / http://$1 - ProxyPreserveHost On - diff --git a/templates/10-loopback-services.cfg.j2 b/templates/10-loopback-services.cfg.j2 deleted file mode 100644 index c5c47e47..00000000 --- a/templates/10-loopback-services.cfg.j2 +++ /dev/null @@ -1,9 +0,0 @@ -auto lo:100 -iface lo:100 inet static - address 172.16.0.1 - netmask 255.255.255.255 - -iface lo:100 inet6 static - address FCAA::1 - netmask 64 - autoconf 0 diff --git a/templates/10periodic.j2 b/templates/10periodic.j2 deleted file mode 100644 index 75870203..00000000 --- a/templates/10periodic.j2 +++ /dev/null @@ -1,4 +0,0 @@ -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Download-Upgradeable-Packages "1"; -APT::Periodic::AutocleanInterval "7"; -APT::Periodic::Unattended-Upgrade "1"; \ No newline at end of file diff --git a/templates/20-ipv6.cfg.j2 b/templates/20-ipv6.cfg.j2 deleted file mode 100644 index 7db27bbb..00000000 --- a/templates/20-ipv6.cfg.j2 +++ /dev/null @@ -1,6 +0,0 @@ -iface eth0 inet6 static - address {{ item.ip_address }} - netmask {{ item.netmask }} - gateway {{ item.gateway }} - autoconf 0 - dns-nameservers 2001:4860:4860::8844 2001:4860:4860::8888 diff --git a/templates/50unattended-upgrades.j2 b/templates/50unattended-upgrades.j2 deleted file mode 100644 index 5f8fb159..00000000 --- a/templates/50unattended-upgrades.j2 +++ /dev/null @@ -1,59 +0,0 @@ -// Automatically upgrade packages from these (origin:archive) pairs -Unattended-Upgrade::Allowed-Origins { - "${distro_id}:${distro_codename}-security"; - "${distro_id}:${distro_codename}-updates"; -// "${distro_id}:${distro_codename}-proposed"; -// "${distro_id}:${distro_codename}-backports"; -}; - -// List of packages to not update (regexp are supported) -Unattended-Upgrade::Package-Blacklist { -// "vim"; -// "libc6"; -// "libc6-dev"; -// "libc6-i686"; -}; - -// This option allows you to control if on a unclean dpkg exit -// unattended-upgrades will automatically run -// dpkg --force-confold --configure -a -// The default is true, to ensure updates keep getting installed -//Unattended-Upgrade::AutoFixInterruptedDpkg "false"; - -// Split the upgrade into the smallest possible chunks so that -// they can be interrupted with SIGUSR1. This makes the upgrade -// a bit slower but it has the benefit that shutdown while a upgrade -// is running is possible (with a small delay) -//Unattended-Upgrade::MinimalSteps "true"; - -// Install all unattended-upgrades when the machine is shuting down -// instead of doing it in the background while the machine is running -// This will (obviously) make shutdown slower -//Unattended-Upgrade::InstallOnShutdown "true"; - -// Send email to this address for problems or packages upgrades -// If empty or unset then no email is sent, make sure that you -// have a working mail setup on your system. A package that provides -// 'mailx' must be installed. E.g. "user@example.com" -//Unattended-Upgrade::Mail "root"; - -// Set this value to "true" to get emails only on errors. Default -// is to always send a mail if Unattended-Upgrade::Mail is set -//Unattended-Upgrade::MailOnlyOnError "true"; - -// Do automatic removal of new unused dependencies after the upgrade -// (equivalent to apt-get autoremove) -//Unattended-Upgrade::Remove-Unused-Dependencies "false"; - -// Automatically reboot *WITHOUT CONFIRMATION* -// if the file /var/run/reboot-required is found after the upgrade -//Unattended-Upgrade::Automatic-Reboot "false"; - -// If automatic reboot is enabled and needed, reboot at the specific -// time instead of immediately -// Default: "now" -//Unattended-Upgrade::Automatic-Reboot-Time "02:00"; - -// Use apt bandwidth limit feature, this example limits the download -// speed to 70kb/sec -//Acquire::http::Dl-Limit "70"; diff --git a/templates/CIS.conf.j2 b/templates/CIS.conf.j2 deleted file mode 100644 index 96b3a595..00000000 --- a/templates/CIS.conf.j2 +++ /dev/null @@ -1,15 +0,0 @@ -*.emerg :omusrmsg:* -mail.* -/var/log/mail -mail.info -/var/log/mail.info -mail.warning -/var/log/mail.warn -mail.err /var/log/mail.err -news.crit -/var/log/news/news.crit -news.err -/var/log/news/news.err -news.notice -/var/log/news/news.notice -*.=warning;*.=err -/var/log/warn -*.crit /var/log/warn -*.*;mail.none;news.none -/var/log/messages -local0,local1.* -/var/log/localmessages -local2,local3.* -/var/log/localmessages -local4,local5.* -/var/log/localmessages -local6,local7.* -/var/log/localmessages \ No newline at end of file diff --git a/templates/adblock.sh b/templates/adblock.sh deleted file mode 100644 index a6a88581..00000000 --- a/templates/adblock.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -#Block ads, malware, etc. - -# Redirect endpoint -ENDPOINT_IP4="0.0.0.0" -ENDPOINT_IP6="::" -IPV6="Y" - -#Delete the old block.hosts to make room for the updates -rm -f /etc/block.hosts - -echo 'Downloading hosts lists...' -#Download and process the files needed to make the lists (enable/add more, if you want) -wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > /tmp/block.build.list -wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list - -#Add black list, if non-empty -if [ -s "/etc/black.list" ] -then - echo 'Adding blacklist...' - awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /etc/black.list >> /tmp/block.build.list -fi - -#Sort the download/black lists -awk '{sub(/\r$/,"");print $1,$2}' /tmp/block.build.list|sort -u > /tmp/block.build.before - -#Filter (if applicable) -if [ -s "/etc/white.list" ] -then - #Filter the blacklist, supressing whitelist matches - # This is relatively slow =-( - echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /etc/block.hosts -else - cat /tmp/block.build.before > /etc/block.hosts -fi - -if [ "$IPV6" = "Y" ] -then - safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') - safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') - echo 'Adding ipv6 support...' - sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /etc/block.hosts -fi - -service dnsmasq restart - -exit 0 diff --git a/templates/audit.rules.j2 b/templates/audit.rules.j2 deleted file mode 100644 index 3464e2a1..00000000 --- a/templates/audit.rules.j2 +++ /dev/null @@ -1,101 +0,0 @@ -# This file contains the auditctl rules that are loaded -# whenever the audit daemon is started via the initscripts. -# The rules are simply the parameters that would be passed -# to auditctl. -# -# First rule - delete all --D - -# Increase the buffers to survive stress events. -# Make this bigger for busy systems --b 320 - -# Feel free to add below this line. See auditctl man page - -# Record Events That Modify Date and Time Information -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S clock_settime -k time-change --a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change -{% endif %} --a always,exit -F arch=b32 -S clock_settime -k time-change --a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change --w /etc/localtime -p wa -k time-change - -# Record Events That Modify User/Group Information --w /etc/group -p wa -k identity --w /etc/passwd -p wa -k identity --w /etc/gshadow -p wa -k identity --w /etc/shadow -p wa -k identity --w /etc/security/opasswd -p wa -k identity - -# Record Events That Modify the System's Network Environment -{% if ansible_architecture == "x86_64" %} --a exit,always -F arch=b64 -S sethostname -S setdomainname -k system-locale -{% endif %} --a exit,always -F arch=b32 -S sethostname -S setdomainname -k system-locale --w /etc/issue -p wa -k system-locale --w /etc/issue.net -p wa -k system-locale --w /etc/hosts -p wa -k system-locale --w /etc/network/interfaces -p wa -k system-locale - -# Collect Login and Logout Events --w /var/log/faillog -p wa -k logins --w /var/log/lastlog -p wa -k logins --w /var/log/tallylog -p wa -k logins - -# Collect Session Initiation Information --w /var/run/utmp -p wa -k session --w /var/log/wtmp -p wa -k session --w /var/log/btmp -p wa -k session - -# Collect Discretionary Access Control Permission Modification Events -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod -{% endif %} --a always,exit -F arch=b32 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b32 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b32 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod - -# Collect Unsuccessful Unauthorized Access Attempts to Files -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access --a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access -{% endif %} --a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access --a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access - -# Collect Use of Privileged Commands -{% if privileged_programs is defined and privileged_programs.stdout_lines|length > 0 %} -{{ privileged_programs.stdout }} -{% endif %} - -# Collect Successful File System Mounts -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S mount -F auid>=500 -F auid!=4294967295 -k mounts -{% endif %} --a always,exit -F arch=b32 -S mount -F auid>=500 -F auid!=4294967295 -k mounts - -# Collect File Deletion Events by User -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete -{% endif %} --a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete - -# Collect Changes to System Administration Scope --w /etc/sudoers -p wa -k scope - -# Collect System Administrator Actions (sudolog) --w /var/log/sudo.log -p wa -k actions - -# Collect Kernel Module Loading and Unloading -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S init_module -S delete_module -k modules -{% endif %} --a always,exit -F arch=b32 -S init_module -S delete_module -k modules --w /sbin/insmod -p x -k modules --w /sbin/rmmod -p x -k modules --w /sbin/modprobe -p x -k modules - --e 2 diff --git a/templates/auditd.conf.j2 b/templates/auditd.conf.j2 deleted file mode 100644 index 24aac738..00000000 --- a/templates/auditd.conf.j2 +++ /dev/null @@ -1,32 +0,0 @@ -# -# This file controls the configuration of the audit daemon -# - -log_file = /var/log/audit/audit.log -log_format = RAW -log_group = root -priority_boost = 4 -flush = INCREMENTAL -freq = 20 -num_logs = 5 -disp_qos = lossy -dispatcher = /sbin/audispd -name_format = NONE -##name = mydomain -max_log_file = 10 -max_log_file_action = keep_logs -space_left = 75 -space_left_action = email -action_mail_acct = {{ auditd_action_mail_acct }} -admin_space_left = 50 -admin_space_left_action = email -disk_full_action = SUSPEND -disk_error_action = SUSPEND -##tcp_listen_port = -tcp_listen_queue = 5 -tcp_max_per_addr = 1 -##tcp_client_ports = 1024-65535 -tcp_client_max_idle = 0 -enable_krb5 = no -krb5_principal = auditd -##krb5_key_file = /etc/audit/audit.key \ No newline at end of file diff --git a/templates/dnsmasq.conf.j2 b/templates/dnsmasq.conf.j2 deleted file mode 100644 index d28cfac3..00000000 --- a/templates/dnsmasq.conf.j2 +++ /dev/null @@ -1,669 +0,0 @@ -# Configuration file for dnsmasq. -# -# Format is one option per line, legal options are the same -# as the long options legal on the command line. See -# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. - -# Listen on this specific port instead of the standard DNS port -# (53). Setting this to zero completely disables DNS function, -# leaving only DHCP and/or TFTP. -#port=5353 - -# The following two options make you a better netizen, since they -# tell dnsmasq to filter out queries which the public DNS cannot -# answer, and which load the servers (especially the root servers) -# unnecessarily. If you have a dial-on-demand link they also stop -# these requests from bringing up the link unnecessarily. - -# Never forward plain names (without a dot or domain part) -#domain-needed -# Never forward addresses in the non-routed address spaces. -#bogus-priv - -# Uncomment these to enable DNSSEC validation and caching: -# (Requires dnsmasq to be built with DNSSEC option.) -#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf -#dnssec - -# Replies which are not DNSSEC signed may be legitimate, because the domain -# is unsigned, or may be forgeries. Setting this option tells dnsmasq to -# check that an unsigned reply is OK, by finding a secure proof that a DS -# record somewhere between the root and the domain does not exist. -# The cost of setting this is that even queries in unsigned domains will need -# one or more extra DNS queries to verify. -#dnssec-check-unsigned - -# Uncomment this to filter useless windows-originated DNS requests -# which can trigger dial-on-demand links needlessly. -# Note that (amongst other things) this blocks all SRV requests, -# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk. -# This option only affects forwarding, SRV records originating for -# dnsmasq (via srv-host= lines) are not suppressed by it. -#filterwin2k - -# Change this line if you want dns to get its upstream servers from -# somewhere other that /etc/resolv.conf -#resolv-file= - -# By default, dnsmasq will send queries to any of the upstream -# servers it knows about and tries to favour servers to are known -# to be up. Uncommenting this forces dnsmasq to try each query -# with each server strictly in the order they appear in -# /etc/resolv.conf -#strict-order - -# If you don't want dnsmasq to read /etc/resolv.conf or any other -# file, getting its servers from this file instead (see below), then -# uncomment this. -#no-resolv - -# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv -# files for changes and re-read them then uncomment this. -#no-poll - -# Add other name servers here, with domain specs if they are for -# non-public domains. -#server=/localnet/192.168.0.1 - -# Example of routing PTR queries to nameservers: this will send all -# address->name queries for 192.168.3/24 to nameserver 10.1.2.3 -#server=/3.168.192.in-addr.arpa/10.1.2.3 - -# Add local-only domains here, queries in these domains are answered -# from /etc/hosts or DHCP only. -#local=/localnet/ - -# Add domains which you want to force to an IP address here. -# The example below send any host in double-click.net to a local -# web-server. -#address=/double-click.net/127.0.0.1 - -# --address (and --server) work with IPv6 addresses too. -#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 - -# Add the IPs of all queries to yahoo.com, google.com, and their -# subdomains to the vpn and search ipsets: -#ipset=/yahoo.com/google.com/vpn,search - -# You can control how dnsmasq talks to a server: this forces -# queries to 10.1.2.3 to be routed via eth1 -# server=10.1.2.3@eth1 -server=8.8.8.8 -server=8.8.4.4 - -# and this sets the source (ie local) address used to talk to -# 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that -# IP on the machine, obviously). -# server=10.1.2.3@192.168.1.1#55 - -# If you want dnsmasq to change uid and gid to something other -# than the default, edit the following lines. -user=nobody -group=nogroup - -# If you want dnsmasq to listen for DHCP and DNS requests only on -# specified interfaces (and the loopback) give the name of the -# interface (eg eth0) here. -# Repeat the line for more than one interface. -#interface=lo -# Or you can specify which interface _not_ to listen on -#except-interface= -# Or which to listen on by address (remember to include 127.0.0.1 if -# you use this.) -listen-address=172.16.0.1,127.0.0.1,FCAA::1 -# If you want dnsmasq to provide only DNS service on an interface, -# configure it as shown above, and then use the following line to -# disable DHCP and TFTP on it. -#no-dhcp-interface= - -# On systems which support it, dnsmasq binds the wildcard address, -# even when it is listening on only some interfaces. It then discards -# requests that it shouldn't reply to. This has the advantage of -# working even when interfaces come and go and change address. If you -# want dnsmasq to really bind only the interfaces it is listening on, -# uncomment this option. About the only time you may need this is when -# running another nameserver on the same machine. -bind-interfaces - -# If you don't want dnsmasq to read /etc/hosts, uncomment the -# following line. -#no-hosts -# or if you want it to read another file, as well as /etc/hosts, use -# this. -addn-hosts=/etc/block.hosts - -# Set this (and domain: see below) if you want to have a domain -# automatically added to simple names in a hosts-file. -#expand-hosts - -# Set the domain for dnsmasq. this is optional, but if it is set, it -# does the following things. -# 1) Allows DHCP hosts to have fully qualified domain names, as long -# as the domain part matches this setting. -# 2) Sets the "domain" DHCP option thereby potentially setting the -# domain of all systems configured by DHCP -# 3) Provides the domain part for "expand-hosts" -#domain=thekelleys.org.uk - -# Set a different domain for a particular subnet -#domain=wireless.thekelleys.org.uk,192.168.2.0/24 - -# Same idea, but range rather then subnet -#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200 - -# Uncomment this to enable the integrated DHCP server, you need -# to supply the range of addresses available for lease and optionally -# a lease time. If you have more than one network, you will need to -# repeat this for each network on which you want to supply DHCP -# service. -#dhcp-range=192.168.0.50,192.168.0.150,12h - -# This is an example of a DHCP range where the netmask is given. This -# is needed for networks we reach the dnsmasq DHCP server via a relay -# agent. If you don't know what a DHCP relay agent is, you probably -# don't need to worry about this. -#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h - -# This is an example of a DHCP range which sets a tag, so that -# some DHCP options may be set only for this network. -#dhcp-range=set:red,192.168.0.50,192.168.0.150 - -# Use this DHCP range only when the tag "green" is set. -#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h - -# Specify a subnet which can't be used for dynamic address allocation, -# is available for hosts with matching --dhcp-host lines. Note that -# dhcp-host declarations will be ignored unless there is a dhcp-range -# of some type for the subnet in question. -# In this case the netmask is implied (it comes from the network -# configuration on the machine running dnsmasq) it is possible to give -# an explicit netmask instead. -#dhcp-range=192.168.0.0,static - -# Enable DHCPv6. Note that the prefix-length does not need to be specified -# and defaults to 64 if missing/ -#dhcp-range=1234::2, 1234::500, 64, 12h - -# Do Router Advertisements, BUT NOT DHCP for this subnet. -#dhcp-range=1234::, ra-only - -# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and -# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack -# hosts. Use the DHCPv4 lease to derive the name, network segment and -# MAC address and assume that the host will also have an -# IPv6 address calculated using the SLAAC alogrithm. -#dhcp-range=1234::, ra-names - -# Do Router Advertisements, BUT NOT DHCP for this subnet. -# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.) -#dhcp-range=1234::, ra-only, 48h - -# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA -# so that clients can use SLAAC addresses as well as DHCP ones. -#dhcp-range=1234::2, 1234::500, slaac - -# Do Router Advertisements and stateless DHCP for this subnet. Clients will -# not get addresses from DHCP, but they will get other configuration information. -# They will use SLAAC for addresses. -#dhcp-range=1234::, ra-stateless - -# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses -# from DHCPv4 leases. -#dhcp-range=1234::, ra-stateless, ra-names - -# Do router advertisements for all subnets where we're doing DHCPv6 -# Unless overriden by ra-stateless, ra-names, et al, the router -# advertisements will have the M and O bits set, so that the clients -# get addresses and configuration from DHCPv6, and the A bit reset, so the -# clients don't use SLAAC addresses. -#enable-ra - -# Supply parameters for specified hosts using DHCP. There are lots -# of valid alternatives, so we will give examples of each. Note that -# IP addresses DO NOT have to be in the range given above, they just -# need to be on the same network. The order of the parameters in these -# do not matter, it's permissible to give name, address and MAC in any -# order. - -# Always allocate the host with Ethernet address 11:22:33:44:55:66 -# The IP address 192.168.0.60 -#dhcp-host=11:22:33:44:55:66,192.168.0.60 - -# Always set the name of the host with hardware address -# 11:22:33:44:55:66 to be "fred" -#dhcp-host=11:22:33:44:55:66,fred - -# Always give the host with Ethernet address 11:22:33:44:55:66 -# the name fred and IP address 192.168.0.60 and lease time 45 minutes -#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m - -# Give a host with Ethernet address 11:22:33:44:55:66 or -# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume -# that these two Ethernet interfaces will never be in use at the same -# time, and give the IP address to the second, even if it is already -# in use by the first. Useful for laptops with wired and wireless -# addresses. -#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60 - -# Give the machine which says its name is "bert" IP address -# 192.168.0.70 and an infinite lease -#dhcp-host=bert,192.168.0.70,infinite - -# Always give the host with client identifier 01:02:02:04 -# the IP address 192.168.0.60 -#dhcp-host=id:01:02:02:04,192.168.0.60 - -# Always give the Infiniband interface with hardware address -# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the -# ip address 192.168.0.61. The client id is derived from the prefix -# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of -# hex digits of the hardware address. -#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61 - -# Always give the host with client identifier "marjorie" -# the IP address 192.168.0.60 -#dhcp-host=id:marjorie,192.168.0.60 - -# Enable the address given for "judge" in /etc/hosts -# to be given to a machine presenting the name "judge" when -# it asks for a DHCP lease. -#dhcp-host=judge - -# Never offer DHCP service to a machine whose Ethernet -# address is 11:22:33:44:55:66 -#dhcp-host=11:22:33:44:55:66,ignore - -# Ignore any client-id presented by the machine with Ethernet -# address 11:22:33:44:55:66. This is useful to prevent a machine -# being treated differently when running under different OS's or -# between PXE boot and OS boot. -#dhcp-host=11:22:33:44:55:66,id:* - -# Send extra options which are tagged as "red" to -# the machine with Ethernet address 11:22:33:44:55:66 -#dhcp-host=11:22:33:44:55:66,set:red - -# Send extra options which are tagged as "red" to -# any machine with Ethernet address starting 11:22:33: -#dhcp-host=11:22:33:*:*:*,set:red - -# Give a fixed IPv6 address and name to client with -# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 -# Note the MAC addresses CANNOT be used to identify DHCPv6 clients. -# Note also the they [] around the IPv6 address are obilgatory. -#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] - -# Ignore any clients which are not specified in dhcp-host lines -# or /etc/ethers. Equivalent to ISC "deny unknown-clients". -# This relies on the special "known" tag which is set when -# a host is matched. -#dhcp-ignore=tag:!known - -# Send extra options which are tagged as "red" to any machine whose -# DHCP vendorclass string includes the substring "Linux" -#dhcp-vendorclass=set:red,Linux - -# Send extra options which are tagged as "red" to any machine one -# of whose DHCP userclass strings includes the substring "accounts" -#dhcp-userclass=set:red,accounts - -# Send extra options which are tagged as "red" to any machine whose -# MAC address matches the pattern. -#dhcp-mac=set:red,00:60:8C:*:*:* - -# If this line is uncommented, dnsmasq will read /etc/ethers and act -# on the ethernet-address/IP pairs found there just as if they had -# been given as --dhcp-host options. Useful if you keep -# MAC-address/host mappings there for other purposes. -#read-ethers - -# Send options to hosts which ask for a DHCP lease. -# See RFC 2132 for details of available options. -# Common options can be given to dnsmasq by name: -# run "dnsmasq --help dhcp" to get a list. -# Note that all the common settings, such as netmask and -# broadcast address, DNS server and default route, are given -# sane defaults by dnsmasq. You very likely will not need -# any dhcp-options. If you use Windows clients and Samba, there -# are some options which are recommended, they are detailed at the -# end of this section. - -# Override the default route supplied by dnsmasq, which assumes the -# router is the same machine as the one running dnsmasq. -#dhcp-option=3,1.2.3.4 - -# Do the same thing, but using the option name -#dhcp-option=option:router,1.2.3.4 - -# Override the default route supplied by dnsmasq and send no default -# route at all. Note that this only works for the options sent by -# default (1, 3, 6, 12, 28) the same line will send a zero-length option -# for all other option numbers. -#dhcp-option=3 - -# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5 -#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5 - -# Send DHCPv6 option. Note [] around IPv6 addresses. -#dhcp-option=option6:dns-server,[1234::77],[1234::88] - -# Send DHCPv6 option for namservers as the machine running -# dnsmasq and another. -#dhcp-option=option6:dns-server,[::],[1234::88] - -# Ask client to poll for option changes every six hours. (RFC4242) -#dhcp-option=option6:information-refresh-time,6h - -# Set option 58 client renewal time (T1). Defaults to half of the -# lease time if not specified. (RFC2132) -#dhcp-option=option:T1:1m - -# Set option 59 rebinding time (T2). Defaults to 7/8 of the -# lease time if not specified. (RFC2132) -#dhcp-option=option:T2:2m - -# Set the NTP time server address to be the same machine as -# is running dnsmasq -#dhcp-option=42,0.0.0.0 - -# Set the NIS domain name to "welly" -#dhcp-option=40,welly - -# Set the default time-to-live to 50 -#dhcp-option=23,50 - -# Set the "all subnets are local" flag -#dhcp-option=27,1 - -# Send the etherboot magic flag and then etherboot options (a string). -#dhcp-option=128,e4:45:74:68:00:00 -#dhcp-option=129,NIC=eepro100 - -# Specify an option which will only be sent to the "red" network -# (see dhcp-range for the declaration of the "red" network) -# Note that the tag: part must precede the option: part. -#dhcp-option = tag:red, option:ntp-server, 192.168.1.1 - -# The following DHCP options set up dnsmasq in the same way as is specified -# for the ISC dhcpcd in -# http://www.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt -# adapted for a typical dnsmasq installation where the host running -# dnsmasq is also the host running samba. -# you may want to uncomment some or all of them if you use -# Windows clients and Samba. -#dhcp-option=19,0 # option ip-forwarding off -#dhcp-option=44,0.0.0.0 # set netbios-over-TCP/IP nameserver(s) aka WINS server(s) -#dhcp-option=45,0.0.0.0 # netbios datagram distribution server -#dhcp-option=46,8 # netbios node type - -# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave. -#dhcp-option=252,"\n" - -# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client -# probably doesn't support this...... -#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com - -# Send RFC-3442 classless static routes (note the netmask encoding) -#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8 - -# Send vendor-class specific options encapsulated in DHCP option 43. -# The meaning of the options is defined by the vendor-class so -# options are sent only when the client supplied vendor class -# matches the class given here. (A substring match is OK, so "MSFT" -# matches "MSFT" and "MSFT 5.0"). This example sets the -# mtftp address to 0.0.0.0 for PXEClients. -#dhcp-option=vendor:PXEClient,1,0.0.0.0 - -# Send microsoft-specific option to tell windows to release the DHCP lease -# when it shuts down. Note the "i" flag, to tell dnsmasq to send the -# value as a four-byte integer - that's what microsoft wants. See -# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true -#dhcp-option=vendor:MSFT,2,1i - -# Send the Encapsulated-vendor-class ID needed by some configurations of -# Etherboot to allow is to recognise the DHCP server. -#dhcp-option=vendor:Etherboot,60,"Etherboot" - -# Send options to PXELinux. Note that we need to send the options even -# though they don't appear in the parameter request list, so we need -# to use dhcp-option-force here. -# See http://syslinux.zytor.com/pxe.php#special for details. -# Magic number - needed before anything else is recognised -#dhcp-option-force=208,f1:00:74:7e -# Configuration file name -#dhcp-option-force=209,configs/common -# Path prefix -#dhcp-option-force=210,/tftpboot/pxelinux/files/ -# Reboot time. (Note 'i' to send 32-bit value) -#dhcp-option-force=211,30i - -# Set the boot filename for netboot/PXE. You will only need -# this is you want to boot machines over the network and you will need -# a TFTP server; either dnsmasq's built in TFTP server or an -# external one. (See below for how to enable the TFTP server.) -#dhcp-boot=pxelinux.0 - -# The same as above, but use custom tftp-server instead machine running dnsmasq -#dhcp-boot=pxelinux,server.name,192.168.1.100 - -# Boot for Etherboot gPXE. The idea is to send two different -# filenames, the first loads gPXE, and the second tells gPXE what to -# load. The dhcp-match sets the gpxe tag for requests from gPXE. -#dhcp-match=set:gpxe,175 # gPXE sends a 175 option. -#dhcp-boot=tag:!gpxe,undionly.kpxe -#dhcp-boot=mybootimage - -# Encapsulated options for Etherboot gPXE. All the options are -# encapsulated within option 175 -#dhcp-option=encap:175, 1, 5b # priority code -#dhcp-option=encap:175, 176, 1b # no-proxydhcp -#dhcp-option=encap:175, 177, string # bus-id -#dhcp-option=encap:175, 189, 1b # BIOS drive code -#dhcp-option=encap:175, 190, user # iSCSI username -#dhcp-option=encap:175, 191, pass # iSCSI password - -# Test for the architecture of a netboot client. PXE clients are -# supposed to send their architecture as option 93. (See RFC 4578) -#dhcp-match=peecees, option:client-arch, 0 #x86-32 -#dhcp-match=itanics, option:client-arch, 2 #IA64 -#dhcp-match=hammers, option:client-arch, 6 #x86-64 -#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64 - -# Do real PXE, rather than just booting a single file, this is an -# alternative to dhcp-boot. -#pxe-prompt="What system shall I netboot?" -# or with timeout before first available action is taken: -#pxe-prompt="Press F8 for menu.", 60 - -# Available boot services. for PXE. -#pxe-service=x86PC, "Boot from local disk" - -# Loads /pxelinux.0 from dnsmasq TFTP server. -#pxe-service=x86PC, "Install Linux", pxelinux - -# Loads /pxelinux.0 from TFTP server at 1.2.3.4. -# Beware this fails on old PXE ROMS. -#pxe-service=x86PC, "Install Linux", pxelinux, 1.2.3.4 - -# Use bootserver on network, found my multicast or broadcast. -#pxe-service=x86PC, "Install windows from RIS server", 1 - -# Use bootserver at a known IP address. -#pxe-service=x86PC, "Install windows from RIS server", 1, 1.2.3.4 - -# If you have multicast-FTP available, -# information for that can be passed in a similar way using options 1 -# to 5. See page 19 of -# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf - - -# Enable dnsmasq's built-in TFTP server -#enable-tftp - -# Set the root directory for files available via FTP. -#tftp-root=/var/ftpd - -# Do not abort if the tftp-root is unavailable -#tftp-no-fail - -# Make the TFTP server more secure: with this set, only files owned by -# the user dnsmasq is running as will be send over the net. -#tftp-secure - -# This option stops dnsmasq from negotiating a larger blocksize for TFTP -# transfers. It will slow things down, but may rescue some broken TFTP -# clients. -#tftp-no-blocksize - -# Set the boot file name only when the "red" tag is set. -#dhcp-boot=tag:red,pxelinux.red-net - -# An example of dhcp-boot with an external TFTP server: the name and IP -# address of the server are given after the filename. -# Can fail with old PXE ROMS. Overridden by --pxe-service. -#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3 - -# If there are multiple external tftp servers having a same name -# (using /etc/hosts) then that name can be specified as the -# tftp_servername (the third option to dhcp-boot) and in that -# case dnsmasq resolves this name and returns the resultant IP -# addresses in round robin fasion. This facility can be used to -# load balance the tftp load among a set of servers. -#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name - -# Set the limit on DHCP leases, the default is 150 -#dhcp-lease-max=150 - -# The DHCP server needs somewhere on disk to keep its lease database. -# This defaults to a sane location, but if you want to change it, use -# the line below. -#dhcp-leasefile=/var/lib/misc/dnsmasq.leases - -# Set the DHCP server to authoritative mode. In this mode it will barge in -# and take over the lease for any client which broadcasts on the network, -# whether it has a record of the lease or not. This avoids long timeouts -# when a machine wakes up on a new network. DO NOT enable this if there's -# the slightest chance that you might end up accidentally configuring a DHCP -# server for your campus/company accidentally. The ISC server uses -# the same option, and this URL provides more information: -# http://www.isc.org/files/auth.html -#dhcp-authoritative - -# Run an executable when a DHCP lease is created or destroyed. -# The arguments sent to the script are "add" or "del", -# then the MAC address, the IP address and finally the hostname -# if there is one. -#dhcp-script=/bin/echo - -# Set the cachesize here. -#cache-size=150 - -# If you want to disable negative caching, uncomment this. -#no-negcache - -# Normally responses which come from /etc/hosts and the DHCP lease -# file have Time-To-Live set as zero, which conventionally means -# do not cache further. If you are happy to trade lower load on the -# server for potentially stale date, you can set a time-to-live (in -# seconds) here. -#local-ttl= - -# If you want dnsmasq to detect attempts by Verisign to send queries -# to unregistered .com and .net hosts to its sitefinder service and -# have dnsmasq instead return the correct NXDOMAIN response, uncomment -# this line. You can add similar lines to do the same for other -# registries which have implemented wildcard A records. -#bogus-nxdomain=64.94.110.11 - -# If you want to fix up DNS results from upstream servers, use the -# alias option. This only works for IPv4. -# This alias makes a result of 1.2.3.4 appear as 5.6.7.8 -#alias=1.2.3.4,5.6.7.8 -# and this maps 1.2.3.x to 5.6.7.x -#alias=1.2.3.0,5.6.7.0,255.255.255.0 -# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40 -#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0 - -# Change these lines if you want dnsmasq to serve MX records. - -# Return an MX record named "maildomain.com" with target -# servermachine.com and preference 50 -#mx-host=maildomain.com,servermachine.com,50 - -# Set the default target for MX records created using the localmx option. -#mx-target=servermachine.com - -# Return an MX record pointing to the mx-target for all local -# machines. -#localmx - -# Return an MX record pointing to itself for all local machines. -#selfmx - -# Change the following lines if you want dnsmasq to serve SRV -# records. These are useful if you want to serve ldap requests for -# Active Directory and other windows-originated DNS requests. -# See RFC 2782. -# You may add multiple srv-host lines. -# The fields are ,,,, -# If the domain part if missing from the name (so that is just has the -# service and protocol sections) then the domain given by the domain= -# config option is used. (Note that expand-hosts does not need to be -# set for this to work.) - -# A SRV record sending LDAP for the example.com domain to -# ldapserver.example.com port 389 -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389 - -# A SRV record sending LDAP for the example.com domain to -# ldapserver.example.com port 389 (using domain=) -#domain=example.com -#srv-host=_ldap._tcp,ldapserver.example.com,389 - -# Two SRV records for LDAP, each with different priorities -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1 -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2 - -# A SRV record indicating that there is no LDAP server for the domain -# example.com -#srv-host=_ldap._tcp.example.com - -# The following line shows how to make dnsmasq serve an arbitrary PTR -# record. This is useful for DNS-SD. (Note that the -# domain-name expansion done for SRV records _does_not -# occur for PTR records.) -#ptr-record=_http._tcp.dns-sd-services,"New Employee Page._http._tcp.dns-sd-services" - -# Change the following lines to enable dnsmasq to serve TXT records. -# These are used for things like SPF and zeroconf. (Note that the -# domain-name expansion done for SRV records _does_not -# occur for TXT records.) - -#Example SPF. -#txt-record=example.com,"v=spf1 a -all" - -#Example zeroconf -#txt-record=_http._tcp.example.com,name=value,paper=A4 - -# Provide an alias for a "local" DNS name. Note that this _only_ works -# for targets which are names from DHCP or /etc/hosts. Give host -# "bert" another name, bertrand -#cname=bertand,bert - -# For debugging purposes, log each DNS query as it passes through -# dnsmasq. -#log-queries - -# Log lots of extra information about DHCP transactions. -#log-dhcp - -# Include another lot of configuration options. -#conf-file=/etc/dnsmasq.more.conf -#conf-dir=/etc/dnsmasq.d - -# Include all the files in a directory except those ending in .bak -#conf-dir=/etc/dnsmasq.d,.bak - -# Include all files in a directory which end in .conf -#conf-dir=/etc/dnsmasq.d/,*.conf -# diff --git a/templates/easy-rsa.vars.j2 b/templates/easy-rsa.vars.j2 deleted file mode 100644 index f46993fb..00000000 --- a/templates/easy-rsa.vars.j2 +++ /dev/null @@ -1,198 +0,0 @@ -# Easy-RSA 3 parameter settings - -# NOTE: If you installed Easy-RSA from your distro's package manager, don't edit -# this file in place -- instead, you should copy the entire easy-rsa directory -# to another location so future upgrades don't wipe out your changes. - -# HOW TO USE THIS FILE -# -# vars.example contains built-in examples to Easy-RSA settings. You MUST name -# this file 'vars' if you want it to be used as a configuration file. If you do -# not, it WILL NOT be automatically read when you call easyrsa commands. -# -# It is not necessary to use this config file unless you wish to change -# operational defaults. These defaults should be fine for many uses without the -# need to copy and edit the 'vars' file. -# -# All of the editable settings are shown commented and start with the command -# 'set_var' -- this means any set_var command that is uncommented has been -# modified by the user. If you're happy with a default, there is no need to -# define the value to its default. - -# NOTES FOR WINDOWS USERS -# -# Paths for Windows *MUST* use forward slashes, or optionally double-esscaped -# backslashes (single forward slashes are recommended.) This means your path to -# the openssl binary might look like this: -# "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" - -# A little housekeeping: DON'T EDIT THIS SECTION -# -# Easy-RSA 3.x doesn't source into the environment directly. -# Complain if a user tries to do this: -if [ -z "$EASYRSA_CALLER" ]; then - echo "You appear to be sourcing an Easy-RSA 'vars' file." >&2 - echo "This is no longer necessary and is disallowed. See the section called" >&2 - echo "'How to use this file' near the top comments for more details." >&2 - return 1 -fi - -# DO YOUR EDITS BELOW THIS POINT - -# This variable should point to the top level of the easy-rsa tree. By default, -# this is taken to be the directory you are currently in. - -set_var EASYRSA "{{ easyrsa_dir }}/easyrsa3/" - -# If your OpenSSL command is not in the system PATH, you will need to define the -# path to it here. Normally this means a full path to the executable, otherwise -# you could have left it undefined here and the shown default would be used. -# -# Windows users, remember to use paths with forward-slashes (or escaped -# back-slashes.) Windows users should declare the full path to the openssl -# binary here if it is not in their system PATH. - -#set_var EASYRSA_OPENSSL "openssl" -# -# This sample is in Windows syntax -- edit it for your path if not using PATH: -#set_var EASYRSA_OPENSSL "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" - -# Edit this variable to point to your soon-to-be-created key directory. -# -# WARNING: init-pki will do a rm -rf on this directory so make sure you define -# it correctly! (Interactive mode will prompt before acting.) - -set_var EASYRSA_PKI "$EASYRSA/pki" - -# Define X509 DN mode. -# This is used to adjust what elements are included in the Subject field as the DN -# (this is the "Distinguished Name.") -# Note that in cn_only mode the Organizational fields further below aren't used. -# -# Choices are: -# cn_only - use just a CN value -# org - use the "traditional" Country/Province/City/Org/OU/email/CN format - -set_var EASYRSA_DN "cn_only" - -# Organizational fields (used with 'org' mode and ignored in 'cn_only' mode.) -# These are the default values for fields which will be placed in the -# certificate. Don't leave any of these fields blank, although interactively -# you may omit any specific field by typing the "." symbol (not valid for -# email.) - -#set_var EASYRSA_REQ_COUNTRY "US" -#set_var EASYRSA_REQ_PROVINCE "California" -#set_var EASYRSA_REQ_CITY "San Francisco" -#set_var EASYRSA_REQ_ORG "Copyleft Certificate Co" -#set_var EASYRSA_REQ_EMAIL "me@example.net" -#set_var EASYRSA_REQ_OU "My Organizational Unit" - -# Choose a size in bits for your keypairs. The recommended value is 2048. Using -# 2048-bit keys is considered more than sufficient for many years into the -# future. Larger keysizes will slow down TLS negotiation and make key/DH param -# generation take much longer. Values up to 4096 should be accepted by most -# software. Only used when the crypto alg is rsa (see below.) - -# set_var EASYRSA_KEY_SIZE 2048 - -# The default crypto mode is rsa; ec can enable elliptic curve support. -# Note that not all software supports ECC, so use care when enabling it. -# Choices for crypto alg are: (each in lower-case) -# * rsa -# * ec - -set_var EASYRSA_ALGO ec - -# Define the named curve, used in ec mode only: - -set_var EASYRSA_CURVE prime256v1 - -# In how many days should the root CA key expire? - -set_var EASYRSA_CA_EXPIRE {{ easyrsa_ca_expire }} - -# In how many days should certificates expire? - -set_var EASYRSA_CERT_EXPIRE {{ easyrsa_cert_expire }} - -# How many days until the next CRL publish date? Note that the CRL can still be -# parsed after this timeframe passes. It is only used for an expected next -# publication date. - -#set_var EASYRSA_CRL_DAYS 180 - -# Support deprecated "Netscape" extensions? (choices "yes" or "no".) The default -# is "no" to discourage use of deprecated extensions. If you require this -# feature to use with --ns-cert-type, set this to "yes" here. This support -# should be replaced with the more modern --remote-cert-tls feature. If you do -# not use --ns-cert-type in your configs, it is safe (and recommended) to leave -# this defined to "no". When set to "yes", server-signed certs get the -# nsCertType=server attribute, and also get any NS_COMMENT defined below in the -# nsComment field. - -#set_var EASYRSA_NS_SUPPORT "no" - -# When NS_SUPPORT is set to "yes", this field is added as the nsComment field. -# Set this blank to omit it. With NS_SUPPORT set to "no" this field is ignored. - -#set_var EASYRSA_NS_COMMENT "Easy-RSA Generated Certificate" - -# A temp file used to stage cert extensions during signing. The default should -# be fine for most users; however, some users might want an alternative under a -# RAM-based FS, such as /dev/shm or /tmp on some systems. - -#set_var EASYRSA_TEMP_FILE "$EASYRSA_PKI/extensions.temp" - -# !! -# NOTE: ADVANCED OPTIONS BELOW THIS POINT -# PLAY WITH THEM AT YOUR OWN RISK -# !! - -# Broken shell command aliases: If you have a largely broken shell that is -# missing any of these POSIX-required commands used by Easy-RSA, you will need -# to define an alias to the proper path for the command. The symptom will be -# some form of a 'command not found' error from your shell. This means your -# shell is BROKEN, but you can hack around it here if you really need. These -# shown values are not defaults: it is up to you to know what you're doing if -# you touch these. -# -#alias awk="/alt/bin/awk" -#alias cat="/alt/bin/cat" - -# X509 extensions directory: -# If you want to customize the X509 extensions used, set the directory to look -# for extensions here. Each cert type you sign must have a matching filename, -# and an optional file named 'COMMON' is included first when present. Note that -# when undefined here, default behaviour is to look in $EASYRSA_PKI first, then -# fallback to $EASYRSA for the 'x509-types' dir. You may override this -# detection with an explicit dir here. -# -#set_var EASYRSA_EXT_DIR "$EASYRSA/x509-types" - -# OpenSSL config file: -# If you need to use a specific openssl config file, you can reference it here. -# Normally this file is auto-detected from a file named openssl-1.0.cnf from the -# EASYRSA_PKI or EASYRSA dir (in that order.) NOTE that this file is Easy-RSA -# specific and you cannot just use a standard config file, so this is an -# advanced feature. - -set_var EASYRSA_SSL_CONF "$EASYRSA/openssl-1.0.cnf" - -# Default CN: -# This is best left alone. Interactively you will set this manually, and BATCH -# callers are expected to set this themselves. - -set_var EASYRSA_REQ_CN "{{ ansible_ssh_host }}" - -# Cryptographic digest to use. -# Do not change this default unless you understand the security implications. -# Valid choices include: md5, sha1, sha256, sha224, sha384, sha512 - -#set_var EASYRSA_DIGEST "sha256" - -# Batch mode. Leave this disabled unless you intend to call Easy-RSA explicitly -# in batch mode without any user input, confirmation on dangerous operations, -# or most output. Setting this to any non-blank string enables batch mode. - -set_var EASYRSA_BATCH "{{ ansible_ssh_host }}" diff --git a/templates/ipsec.conf.j2 b/templates/ipsec.conf.j2 deleted file mode 100644 index 8bb61817..00000000 --- a/templates/ipsec.conf.j2 +++ /dev/null @@ -1,34 +0,0 @@ -config setup - uniqueids = never # allow multiple connections per user - charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" - -conn %default - dpdaction=clear - dpddelay=35s - dpdtimeout=300s - rekey=no - keyexchange=ikev2 - ike=aes128gcm16-sha2_256-prfsha256-ecp256! - esp=aes128gcm16-sha2_256-ecp256! - compress=yes - fragmentation=yes - - left=%any - leftauth=pubkey - leftid={{ ansible_ssh_host }} - leftcert={{ ansible_ssh_host }}.crt - leftsendcert=always - leftsubnet=0.0.0.0/0,::/0 - - right=%any - rightauth=pubkey - rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if service_dns is defined and service_dns == "N" %} - rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} -{% else %} - rightdns=172.16.0.1 -{% endif %} - - -conn ikev2-pubkey - auto=add diff --git a/templates/ipsec.secrets.j2 b/templates/ipsec.secrets.j2 deleted file mode 100644 index cc208a59..00000000 --- a/templates/ipsec.secrets.j2 +++ /dev/null @@ -1,2 +0,0 @@ -: ECDSA {{ ansible_ssh_host }}.key - diff --git a/templates/mobileconfig.j2 b/templates/mobileconfig.j2 deleted file mode 100644 index d1a235c6..00000000 --- a/templates/mobileconfig.j2 +++ /dev/null @@ -1,144 +0,0 @@ - - - - - PayloadContent - - - IKEv2 - - AuthenticationMethod - Certificate - ChildSecurityAssociationParameters - - DiffieHellmanGroup - 19 - EncryptionAlgorithm - AES-128-GCM - IntegrityAlgorithm - SHA2-256 - LifeTimeInMinutes - 1440 - - DeadPeerDetectionRate - Medium - DisableMOBIKE - 0 - DisableRedirect - 0 - EnableCertificateRevocationCheck - 0 - EnablePFS - - IKESecurityAssociationParameters - - DiffieHellmanGroup - 19 - EncryptionAlgorithm - AES-128-GCM - IntegrityAlgorithm - SHA2-256 - LifeTimeInMinutes - 1440 - - LocalIdentifier - {{ item.0 }} - PayloadCertificateUUID - 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 - CertificateType - ECDSA256 - ServerCertificateIssuerCommonName - {{ ansible_ssh_host }} - RemoteAddress - {{ ansible_ssh_host }} - RemoteIdentifier - {{ ansible_ssh_host }} - UseConfigurationAttributeInternalIPSubnet - 0 - - IPv4 - - OverridePrimary - 1 - - PayloadDescription - Configures VPN settings - PayloadDisplayName - VPN - PayloadIdentifier - com.apple.vpn.managed.D247A30B-6023-4C8E-B3E3-FF1910A65E53 - PayloadType - com.apple.vpn.managed - PayloadUUID - D247A30B-6023-4C8E-B3E3-FF1910A65E53 - PayloadVersion - 1 - Proxies - - HTTPEnable - 0 - HTTPSEnable - 0 - - UserDefinedName - {{ ansible_ssh_host }} IKEv2 - VPNType - IKEv2 - - - Password - {{ easyrsa_p12_export_password }} - PayloadCertificateFileName - {{ item.0 }}.p12 - PayloadContent - - {{ item.1.stdout }} - - PayloadDescription - Adds a PKCS#12-formatted certificate - PayloadDisplayName - {{ item.0 }}.p12 - PayloadIdentifier - com.apple.security.pkcs12.1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 - PayloadType - com.apple.security.pkcs12 - PayloadUUID - 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 - PayloadVersion - 1 - - - PayloadCertificateFileName - ca.crt - PayloadContent - - {{ PayloadContentCA.stdout }} - - PayloadDescription - Adds a CA root certificate - PayloadDisplayName - {{ ansible_ssh_host }} - PayloadIdentifier - com.apple.security.root.32EA3AAA-D19E-43EF-B357-608218745A38 - PayloadType - com.apple.security.root - PayloadUUID - 32EA3AAA-D19E-43EF-B357-608218745A38 - PayloadVersion - 1 - - - PayloadDisplayName - {{ ansible_ssh_host }} IKEv2 - PayloadIdentifier - donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BE - PayloadRemovalDisallowed - - PayloadType - Configuration - PayloadUUID - 743B04A8-5725-45A2-B1BB-836F8C16DB0A - PayloadVersion - 1 - - diff --git a/templates/pagespeed.conf.j2 b/templates/pagespeed.conf.j2 deleted file mode 100644 index 3b89b758..00000000 --- a/templates/pagespeed.conf.j2 +++ /dev/null @@ -1,369 +0,0 @@ - - # Turn on mod_pagespeed. To completely disable mod_pagespeed, you - # can set this to "off". - ModPagespeed on - - # We want VHosts to inherit global configuration. - # If this is not included, they'll be independent (except for inherently - # global options), at least for backwards compatibility. - ModPagespeedInheritVHostConfig on - - # Direct Apache to send all HTML output to the mod_pagespeed - # output handler. - AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html - - # If you want mod_pagespeed process XHTML as well, please uncomment this - # line. - # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml - - # The ModPagespeedFileCachePath directory must exist and be writable - # by the apache user (as specified by the User directive). - ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" - - # LogDir is needed to store various logs, including the statistics log - # required for the console. - ModPagespeedLogDir "/var/log/pagespeed" - - # The locations of SSL Certificates is distribution-dependent. - ModPagespeedSslCertDirectory "/etc/ssl/certs" - - - # If you want, you can use one or more memcached servers as the store for - # the mod_pagespeed cache. - # ModPagespeedMemcachedServers localhost:11211 - - # A portion of the cache can be kept in memory only, to reduce load on disk - # (or memcached) from many small files. - # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 - - # Override the mod_pagespeed 'rewrite level'. The default level - # "CoreFilters" uses a set of rewrite filters that are generally - # safe for most web pages. Most sites should not need to change - # this value and can instead fine-tune the configuration using the - # ModPagespeedDisableFilters and ModPagespeedEnableFilters - # directives, below. Valid values for ModPagespeedRewriteLevel are - # PassThrough, CoreFilters and TestingCoreFilters. - # - ModPagespeedRewriteLevel CoreFilters - - ModPagespeedEnableFilters combine_heads - ModPagespeedEnableFilters combine_javascript - ModPagespeedEnableFilters convert_jpeg_to_webp - ModPagespeedEnableFilters convert_png_to_jpeg - ModPagespeedEnableFilters inline_preview_images - ModPagespeedEnableFilters make_google_analytics_async - ModPagespeedEnableFilters move_css_above_scripts - ModPagespeedEnableFilters move_css_to_head - ModPagespeedEnableFilters resize_mobile_images - ModPagespeedEnableFilters sprite_images - - ModPagespeedEnableFilters defer_iframe - ModPagespeedEnableFilters defer_javascript - ModPagespeedEnableFilters lazyload_images - - # Explicitly disables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, if one - # of the filters in the CoreFilters needs to be disabled for a - # site, that filter can be added to - # ModPagespeedDisableFilters. This directive contains a - # comma-separated list of filter names, and can be repeated. - # - # ModPagespeedDisableFilters rewrite_images - - # Explicitly enables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, filters - # not included in the CoreFilters may be enabled using this - # directive. This directive contains a comma-separated list of - # filter names, and can be repeated. - # - # ModPagespeedEnableFilters rewrite_javascript,rewrite_css - # ModPagespeedEnableFilters collapse_whitespace,elide_attributes - - # Explicitly forbids the enabling of specific filters using either query - # parameters or request headers. This is useful, for example, when we do - # not want the filter to run for performance or security reasons. This - # directive contains a comma-separated list of filter names, and can be - # repeated. - # - # ModPagespeedForbidFilters rewrite_images - - # How long mod_pagespeed will wait to return an optimized resource - # (per flush window) on first request before giving up and returning the - # original (unoptimized) resource. After this deadline is exceeded the - # original resource is returned and the optimization is pushed to the - # background to be completed for future requests. Increasing this value will - # increase page latency, but might reduce load time (for instance on a - # bandwidth-constrained link where it's worth waiting for image - # compression to complete). If the value is less than or equal to zero - # mod_pagespeed will wait indefinitely for the rewrite to complete before - # returning. - # - # ModPagespeedRewriteDeadlinePerFlushMs 10 - - # ModPagespeedDomain - # authorizes rewriting of JS, CSS, and Image files found in this - # domain. By default only resources with the same origin as the - # HTML file are rewritten. For example: - # - ModPagespeedDomain * - # - # This will allow resources found on http://cdn.myhost.com to be - # rewritten in addition to those in the same domain as the HTML. - # - # Other domain-related directives (like ModPagespeedMapRewriteDomain - # and ModPagespeedMapOriginDomain) can also authorize domains. - # - # Wildcards (* and ?) are allowed in the domain specification. Be - # careful when using them as if you rewrite domains that do not - # send you traffic, then the site receiving the traffic will not - # know how to serve the rewritten content. - - # If you use downstream caches such as varnish or proxy_cache for caching - # HTML, you can configure pagespeed to work with these caches correctly - # using the following directives. Note that the values for - # ModPagespeedDownstreamCachePurgeLocationPrefix and - # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here - # in order to force the webmaster to choose appropriate value for these. - # - # ModPagespeedDownstreamCachePurgeLocationPrefix - # ModPagespeedDownstreamCachePurgeMethod PURGE - # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 - # ModPagespeedDownstreamCacheRebeaconingKey - - # Other defaults (cache sizes and thresholds): - # - # ModPagespeedFileCacheSizeKb 102400 - # ModPagespeedFileCacheCleanIntervalMs 3600000 - # ModPagespeedLRUCacheKbPerProcess 1024 - # ModPagespeedLRUCacheByteLimit 16384 - # ModPagespeedCssFlattenMaxBytes 102400 - # ModPagespeedCssInlineMaxBytes 2048 - # ModPagespeedCssImageInlineMaxBytes 0 - # ModPagespeedImageInlineMaxBytes 3072 - # ModPagespeedJsInlineMaxBytes 2048 - # ModPagespeedCssOutlineMinBytes 3000 - # ModPagespeedJsOutlineMinBytes 3000 - # ModPagespeedMaxCombinedCssBytes -1 - # ModPagespeedMaxCombinedJsBytes 92160 - - # Limit the number of inodes in the file cache. Set to 0 for no limit. - # The default value if this paramater is not specified is 0 (no limit). - ModPagespeedFileCacheInodeLimit 500000 - - # Bound the number of images that can be rewritten at any one time; this - # avoids overloading the CPU. Set this to 0 to remove the bound. - # - # ModPagespeedImageMaxRewritesAtOnce 8 - - # You can also customize the number of threads per Apache process - # mod_pagespeed will use to do resource optimization. Plain - # "rewrite threads" are used to do short, latency-sensitive work, - # while "expensive rewrite threads" are used for actual optimization - # work that's more computationally expensive. If you live these unset, - # or use values <= 0 the defaults will be used, which is 1 for both - # values when using non-threaded MPMs (e.g. prefork) and 4 for both - # on threaded MPMs (e.g. worker and event). These settings can only - # be changed globally, and not per virtual host. - # - # ModPagespeedNumRewriteThreads 4 - # ModPagespeedNumExpensiveRewriteThreads 4 - - # Randomly drop rewrites (*) to increase the chance of optimizing - # frequently fetched resources and decrease the chance of optimizing - # infrequently fetched resources. This can reduce CPU load. The default - # value of this parameter is 0 (no drops). 90 means that a resourced - # fetched once has a 10% probability of being optimized while a resource - # that is fetched 50 times has a 99.65% probability of being optimized. - # - # (*) Currently only CSS files and images are randomly dropped. Images - # within CSS files are not randomly dropped. - # - # ModPagespeedRewriteRandomDropPercentage 90 - - # Many filters modify the URLs of resources in HTML files. This is typically - # harmless but pages whose Javascript expects to read or modify the original - # URLs may break. The following parameters prevent filters from modifying - # URLs of their respective types. - # - # ModPagespeedJsPreserveURLs on - # ModPagespeedImagePreserveURLs on - # ModPagespeedCssPreserveURLs on - - # When PreserveURLs is on, it is still possible to enable browser-specific - # optimizations (for example, webp images can be served to browsers that - # will accept them). They'll be served with Vary: Accept or Vary: - # User-Agent headers as appropriate. Note that this may require configuring - # reverse proxy caches such as varnish to handle these headers properly. - # - # ModPagespeedFilters in_place_optimize_for_browser - - # Internet Explorer has difficulty caching resources with Vary: headers. - # They will either be uncached (older IE) or require revalidation. See: - # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx - # As a result we serve them as Cache-Control: private instead by default. - # If you are using a reverse proxy or CDN configured to cache content with - # the Vary: Accept header you should turn this setting off. - # - # ModPagespeedPrivateNotVaryForIE on - - # Settings for image optimization: - # - # Lossy image recompression quality (0 to 100, -1 just strips metadata): - # ModPagespeedImageRecompressionQuality 85 - # - # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedJpegRecompressionQuality -1 - # ModPagespeedJpegRecompressionQualityForSmallScreens 70 - - ModPagespeedJpegRecompressionQuality 75 - - # - # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedWebpRecompressionQuality 80 - # ModPagespeedWebpRecompressionQualityForSmallScreens 70 - # - # Timeout for conversions to WebP format, in - # milliseconds. Negative values mean no timeout is applied. The - # default value is -1: - # ModPagespeedWebpTimeoutMs 5000 - # - # Percent of original image size below which optimized images are retained: - # ModPagespeedImageLimitOptimizedPercent 100 - # - # Percent of original image area below which image resizing will be - # attempted: - # ModPagespeedImageLimitResizeAreaPercent 100 - - # Settings for inline preview images - # - # Setting this to n restricts preview images to the first n images found on - # the page. The default of -1 means preview images can appear anywhere on - # the page (if those images appear above the fold). - # ModPagespeedMaxInlinedPreviewImagesIndex -1 - - # Sets the minimum size in bytes of any image for which a low quality image - # is generated. - # ModPagespeedMinImageSizeLowResolutionBytes 3072 - - # The maximum URL size is generally limited to about 2k characters - # due to IE: See http://support.microsoft.com/kb/208427/EN-US. - # Apache servers by default impose a further limitation of about - # 250 characters per URL segment (text between slashes). - # mod_pagespeed circumvents this limitation, but if you employ - # proxy servers in your path you may need to re-impose it by - # overriding the setting here. The default setting is 1024 - # characters. - # - # ModPagespeedMaxSegmentLength 250 - - # Uncomment this if you want to prevent mod_pagespeed from combining files - # (e.g. CSS files) across paths - # - # ModPagespeedCombineAcrossPaths off - - # Renaming JavaScript URLs can sometimes break them. With this - # option enabled, mod_pagespeed uses a simple heuristic to decide - # not to rename JavaScript that it thinks is introspective. - # - # You can uncomment this to let mod_pagespeed rename all JS files. - # - # ModPagespeedAvoidRenamingIntrospectiveJavascript off - - # Certain common JavaScript libraries are available from Google, which acts - # as a CDN and allows you to benefit from browser caching if a new visitor - # to your site previously visited another site that makes use of the same - # libraries as you do. Enable the following filter to turn on this feature. - # - # ModPagespeedEnableFilters canonicalize_javascript_libraries - - # The following line configures a library that is recognized by - # canonicalize_javascript_libraries. This will have no effect unless you - # enable this filter (generally by uncommenting the last line in the - # previous stanza). The format is: - # ModPagespeedLibrary bytes md5 canonical_url - # Where bytes and md5 are with respect to the *minified* JS; use - # js_minify --print_size_and_hash to obtain this data. - # Note that we can register multiple hashes for the same canonical url; - # we do this if there are versions available that have already been minified - # with more sophisticated tools. - # - # Additional library configuration can be found in - # pagespeed_libraries.conf included in the distribution. You should add - # new entries here, though, so that file can be automatically upgraded. - # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js - - # Explicitly tell mod_pagespeed to load some resources from disk. - # This will speed up load time and update frequency. - # - # This should only be used for static resources which do not need - # specific headers set or other processing by Apache. - # - # Both URL and filesystem path should specify directories and - # filesystem path must be absolute (for now). - # - # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" - - - # Enables server-side instrumentation and statistics. If this rewriter is - # enabled, then each rewritten HTML page will have instrumentation javacript - # added that sends latency beacons to /mod_pagespeed_beacon. These - # statistics can be accessed at /mod_pagespeed_statistics. You must also - # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers - # below. - # - # ModPagespeedEnableFilters add_instrumentation - - # The add_instrumentation filter sends a beacon after the page onload - # handler is called. The user might navigate to a new URL before this. If - # you enable the following directive, the beacon is sent as part of an - # onbeforeunload handler, for pages where navigation happens before the - # onload event. - # - # ModPagespeedReportUnloadTime on - - # Uncomment the following line so that ModPagespeed will not cache or - # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. - # Note that ModPagespeed always respects Vary: headers on html content. - # ModPagespeedRespectVary on - - # Uncomment the following line if you want to disable statistics entirely. - # - # ModPagespeedStatistics off - - # These handlers are central entry-points into the admin pages. - # By default, pagespeed_admin and pagespeed_global_admin present - # the same data, and differ only when - # ModPagespeedUsePerVHostStatistics is enabled. In that case, - # /pagespeed_global_admin sees aggregated data across all vhosts, - # and the /pagespeed_admin sees data only for a particular vhost. - # - # You may insert other "Allow from" lines to add hosts you want to - # allow to look at generated statistics. Another possibility is - # to comment out the "Order" and "Allow" options from the config - # file, to allow any client that can reach your server to access - # and change server state, such as statistics, caches, and - # messages. This might be appropriate in an experimental setup. - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_admin - - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_global_admin - - - # Enable logging of mod_pagespeed statistics, needed for the console. - ModPagespeedStatisticsLogging on - - # Page /mod_pagespeed_message lets you view the latest messages from - # mod_pagespeed, regardless of log-level in your httpd.conf - # ModPagespeedMessageBufferSize is the maximum number of bytes you would - # like to dump to your /mod_pagespeed_message page at one time, - # its default value is 100k bytes. - # Set it to 0 if you want to disable this feature. - ModPagespeedMessageBufferSize 100000 - diff --git a/templates/ports.conf.j2 b/templates/ports.conf.j2 deleted file mode 100644 index 2618436c..00000000 --- a/templates/ports.conf.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# If you just change the port or add more ports here, you will likely also -# have to change the VirtualHost statement in -# /etc/apache2/sites-enabled/000-default.conf - -Listen 172.16.0.1:8080 - - - Listen 172.16.0.1:443 - - - - Listen 172.16.0.1:443 - diff --git a/templates/privoxy_config.j2 b/templates/privoxy_config.j2 deleted file mode 100644 index dd55f0f3..00000000 --- a/templates/privoxy_config.j2 +++ /dev/null @@ -1,2107 +0,0 @@ -# Sample Configuration File for Privoxy -# -# Id: config,v -# -# Copyright (C) 2001-2014 Privoxy Developers http://www.privoxy.org/ -# -#################################################################### -# # -# Table of Contents # -# # -# I. INTRODUCTION # -# II. FORMAT OF THE CONFIGURATION FILE # -# # -# 1. LOCAL SET-UP DOCUMENTATION # -# 2. CONFIGURATION AND LOG FILE LOCATIONS # -# 3. DEBUGGING # -# 4. ACCESS CONTROL AND SECURITY # -# 5. FORWARDING # -# 6. MISCELLANEOUS # -# 7. WINDOWS GUI OPTIONS # -# # -#################################################################### -# -# -# I. INTRODUCTION -# =============== -# -# This file holds Privoxy's main configuration. Privoxy detects -# configuration changes automatically, so you don't have to restart -# it unless you want to load a different configuration file. -# -# The configuration will be reloaded with the first request after -# the change was done, this request itself will still use the old -# configuration, though. In other words: it takes two requests -# before you see the result of your changes. Requests that are -# dropped due to ACL don't trigger reloads. -# -# When starting Privoxy on Unix systems, give the location of this -# file as last argument. On Windows systems, Privoxy will look for -# this file with the name 'config.txt' in the current working -# directory of the Privoxy process. -# -# -# II. FORMAT OF THE CONFIGURATION FILE -# ==================================== -# -# Configuration lines consist of an initial keyword followed by a -# list of values, all separated by whitespace (any number of spaces -# or tabs). For example, -# -# actionsfile default.action -# -# Indicates that the actionsfile is named 'default.action'. -# -# The '#' indicates a comment. Any part of a line following a '#' is -# ignored, except if the '#' is preceded by a '\'. -# -# Thus, by placing a # at the start of an existing configuration -# line, you can make it a comment and it will be treated as if it -# weren't there. This is called "commenting out" an option and can -# be useful. Removing the # again is called "uncommenting". -# -# Note that commenting out an option and leaving it at its default -# are two completely different things! Most options behave very -# differently when unset. See the "Effect if unset" explanation in -# each option's description for details. -# -# Long lines can be continued on the next line by using a `\' as the -# last character. -# -# -# 1. LOCAL SET-UP DOCUMENTATION -# ============================== -# -# If you intend to operate Privoxy for more users than just -# yourself, it might be a good idea to let them know how to reach -# you, what you block and why you do that, your policies, etc. -# -# -# 1.1. user-manual -# ================= -# -# Specifies: -# -# Location of the Privoxy User Manual. -# -# Type of value: -# -# A fully qualified URI -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# http://www.privoxy.org/version/user-manual/ will be used, -# where version is the Privoxy version. -# -# Notes: -# -# The User Manual URI is the single best source of information -# on Privoxy, and is used for help links from some of the -# internal CGI pages. The manual itself is normally packaged -# with the binary distributions, so you probably want to set -# this to a locally installed copy. -# -# Examples: -# -# The best all purpose solution is simply to put the full local -# PATH to where the User Manual is located: -# -# user-manual /usr/share/doc/privoxy/user-manual -# -# The User Manual is then available to anyone with access to -# Privoxy, by following the built-in URL: http:// -# config.privoxy.org/user-manual/ (or the shortcut: http://p.p/ -# user-manual/). -# -# If the documentation is not on the local system, it can be -# accessed from a remote server, as: -# -# user-manual http://example.com/privoxy/user-manual/ -# -# WARNING!!! -# -# If set, this option should be the first option in the -# config file, because it is used while the config file is -# being read. -# -user-manual /usr/share/doc/privoxy/user-manual -# -# 1.2. trust-info-url -# ==================== -# -# Specifies: -# -# A URL to be displayed in the error page that users will see if -# access to an untrusted page is denied. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No links are displayed on the "untrusted" error page. -# -# Notes: -# -# The value of this option only matters if the experimental -# trust mechanism has been activated. (See trustfile below.) -# -# If you use the trust mechanism, it is a good idea to write up -# some on-line documentation about your trust policy and to -# specify the URL(s) here. Use multiple times for multiple URLs. -# -# The URL(s) should be added to the trustfile as well, so users -# don't end up locked out from the information on why they were -# locked out in the first place! -# -#trust-info-url http://www.example.com/why_we_block.html -#trust-info-url http://www.example.com/what_we_allow.html -# -# 1.3. admin-address -# =================== -# -# Specifies: -# -# An email address to reach the Privoxy administrator. -# -# Type of value: -# -# Email address -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No email address is displayed on error pages and the CGI user -# interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -#admin-address privoxy-admin@example.com -# -# 1.4. proxy-info-url -# ==================== -# -# Specifies: -# -# A URL to documentation about the local Privoxy setup, -# configuration or policies. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No link to local documentation is displayed on error pages and -# the CGI user interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -# This URL shouldn't be blocked ;-) -# -#proxy-info-url http://www.example.com/proxy-service.html -# -# 2. CONFIGURATION AND LOG FILE LOCATIONS -# ======================================== -# -# Privoxy can (and normally does) use a number of other files for -# additional configuration, help and logging. This section of the -# configuration file tells Privoxy where to find those other files. -# -# The user running Privoxy, must have read permission for all -# configuration files, and write permission to any files that would -# be modified, such as log files and actions files. -# -# -# 2.1. confdir -# ============= -# -# Specifies: -# -# The directory where the other configuration files are located. -# -# Type of value: -# -# Path name -# -# Default value: -# -# /etc/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -confdir /etc/privoxy -# -# 2.2. templdir -# ============== -# -# Specifies: -# -# An alternative directory where the templates are loaded from. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# The templates are assumed to be located in confdir/template. -# -# Notes: -# -# Privoxy's original templates are usually overwritten with each -# update. Use this option to relocate customized templates that -# should be kept. As template variables might change between -# updates, you shouldn't expect templates to work with Privoxy -# releases other than the one they were part of, though. -# -#templdir . -# -# 2.3. temporary-directory -# ========================= -# -# Specifies: -# -# A directory where Privoxy can create temporary files. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# No temporary files are created, external filters don't work. -# -# Notes: -# -# To execute external filters, Privoxy has to create temporary -# files. This directive specifies the directory the temporary -# files should be written to. -# -# It should be a directory only Privoxy (and trusted users) can -# access. -# -#temporary-directory . -# -# 2.4. logdir -# ============ -# -# Specifies: -# -# The directory where all logging takes place (i.e. where the -# logfile is located). -# -# Type of value: -# -# Path name -# -# Default value: -# -# /var/log/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -logdir /var/log/privoxy -# -# 2.5. actionsfile -# ================= -# -# Specifies: -# -# The actions file(s) to use -# -# Type of value: -# -# Complete file name, relative to confdir -# -# Default values: -# -# match-all.action # Actions that are applied to all sites and maybe overruled later on. -# -# default.action # Main actions file -# -# user.action # User customizations -# -# Effect if unset: -# -# No actions are taken at all. More or less neutral proxying. -# -# Notes: -# -# Multiple actionsfile lines are permitted, and are in fact -# recommended! -# -# The default values are default.action, which is the "main" -# actions file maintained by the developers, and user.action, -# where you can make your personal additions. -# -# Actions files contain all the per site and per URL -# configuration for ad blocking, cookie management, privacy -# considerations, etc. -# -actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on. -actionsfile default.action # Main actions file -actionsfile user.action # User customizations -# -# 2.6. filterfile -# ================ -# -# Specifies: -# -# The filter file(s) to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# default.filter (Unix) or default.filter.txt (Windows) -# -# Effect if unset: -# -# No textual content filtering takes place, i.e. all +filter{name} -# actions in the actions files are turned neutral. -# -# Notes: -# -# Multiple filterfile lines are permitted. -# -# The filter files contain content modification rules that use -# regular expressions. These rules permit powerful changes on -# the content of Web pages, and optionally the headers as well, -# e.g., you could try to disable your favorite JavaScript -# annoyances, re-write the actual displayed text, or just have -# some fun playing buzzword bingo with web pages. -# -# The +filter{name} actions rely on the relevant filter (name) -# to be defined in a filter file! -# -# A pre-defined filter file called default.filter that contains -# a number of useful filters for common problems is included in -# the distribution. See the section on the filter action for a -# list. -# -# It is recommended to place any locally adapted filters into a -# separate file, such as user.filter. -# -filterfile default.filter -filterfile user.filter # User customizations -# -# 2.7. logfile -# ============= -# -# Specifies: -# -# The log file to use -# -# Type of value: -# -# File name, relative to logdir -# -# Default value: -# -# Unset (commented out). When activated: logfile (Unix) or -# privoxy.log (Windows). -# -# Effect if unset: -# -# No logfile is written. -# -# Notes: -# -# The logfile is where all logging and error messages are -# written. The level of detail and number of messages are set -# with the debug option (see below). The logfile can be useful -# for tracking down a problem with Privoxy (e.g., it's not -# blocking an ad you think it should block) and it can help you -# to monitor what your browser is doing. -# -# Depending on the debug options below, the logfile may be a -# privacy risk if third parties can get access to it. As most -# users will never look at it, Privoxy only logs fatal errors by -# default. -# -# For most troubleshooting purposes, you will have to change -# that, please refer to the debugging section for details. -# -# Any log files must be writable by whatever user Privoxy is -# being run as (on Unix, default user id is "privoxy"). -# -# To prevent the logfile from growing indefinitely, it is -# recommended to periodically rotate or shorten it. Many -# operating systems support log rotation out of the box, some -# require additional software to do it. For details, please -# refer to the documentation for your operating system. -# -logfile logfile -# -# 2.8. trustfile -# =============== -# -# Specifies: -# -# The name of the trust file to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# Unset (commented out). When activated: trust (Unix) or -# trust.txt (Windows) -# -# Effect if unset: -# -# The entire trust mechanism is disabled. -# -# Notes: -# -# The trust mechanism is an experimental feature for building -# white-lists and should be used with care. It is NOT -# recommended for the casual user. -# -# If you specify a trust file, Privoxy will only allow access to -# sites that are specified in the trustfile. Sites can be listed -# in one of two ways: -# -# Prepending a ~ character limits access to this site only (and -# any sub-paths within this site), e.g. ~www.example.com allows -# access to ~www.example.com/features/news.html, etc. -# -# Or, you can designate sites as trusted referrers, by -# prepending the name with a + character. The effect is that -# access to untrusted sites will be granted -- but only if a -# link from this trusted referrer was used to get there. The -# link target will then be added to the "trustfile" so that -# future, direct accesses will be granted. Sites added via this -# mechanism do not become trusted referrers themselves (i.e. -# they are added with a ~ designation). There is a limit of 512 -# such entries, after which new entries will not be made. -# -# If you use the + operator in the trust file, it may grow -# considerably over time. -# -# It is recommended that Privoxy be compiled with the -# --disable-force, --disable-toggle and --disable-editor -# options, if this feature is to be used. -# -# Possible applications include limiting Internet access for -# children. -# -#trustfile trust -# -# 3. DEBUGGING -# ============= -# -# These options are mainly useful when tracing a problem. Note that -# you might also want to invoke Privoxy with the --no-daemon command -# line option when debugging. -# -# -# 3.1. debug -# =========== -# -# Specifies: -# -# Key values that determine what information gets logged. -# -# Type of value: -# -# Integer values -# -# Default value: -# -# 0 (i.e.: only fatal errors (that cause Privoxy to exit) are -# logged) -# -# Effect if unset: -# -# Default value is used (see above). -# -# Notes: -# -# The available debug levels are: -# -# debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -# debug 2 # show each connection status -# debug 4 # show I/O status -# debug 8 # show header parsing -# debug 16 # log all data written to the network -# debug 32 # debug force feature -# debug 64 # debug regular expression filters -# debug 128 # debug redirects -# debug 256 # debug GIF de-animation -# debug 512 # Common Log Format -# debug 1024 # Log the destination for requests Privoxy didn't let through, and the reason why. -# debug 2048 # CGI user interface -# debug 4096 # Startup banner and warnings. -# debug 8192 # Non-fatal errors -# debug 32768 # log all data read from the network -# debug 65536 # Log the applying actions -# -# To select multiple debug levels, you can either add them or -# use multiple debug lines. -# -# A debug level of 1 is informative because it will show you -# each request as it happens. 1, 1024, 4096 and 8192 are -# recommended so that you will notice when things go wrong. The -# other levels are probably only of interest if you are hunting -# down a specific problem. They can produce a hell of an output -# (especially 16). -# -# If you are used to the more verbose settings, simply enable -# the debug lines below again. -# -# If you want to use pure CLF (Common Log Format), you should -# set "debug 512" ONLY and not enable anything else. -# -# Privoxy has a hard-coded limit for the length of log messages. -# If it's reached, messages are logged truncated and marked with -# "... [too long, truncated]". -# -# Please don't file any support requests without trying to -# reproduce the problem with increased debug level first. Once -# you read the log messages, you may even be able to solve the -# problem on your own. -# -#debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -#debug 1024 # Actions that are applied to all sites and maybe overruled later on. -#debug 4096 # Startup banner and warnings -#debug 8192 # Non-fatal errors -# -# 3.2. single-threaded -# ===================== -# -# Specifies: -# -# Whether to run only one server thread. -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Multi-threaded (or, where unavailable: forked) operation, i.e. -# the ability to serve multiple requests simultaneously. -# -# Notes: -# -# This option is only there for debugging purposes. It will -# drastically reduce performance. -# -#single-threaded 1 -# -# 3.3. hostname -# ============== -# -# Specifies: -# -# The hostname shown on the CGI pages. -# -# Type of value: -# -# Text -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# The hostname provided by the operating system is used. -# -# Notes: -# -# On some misconfigured systems resolving the hostname fails or -# takes too much time and slows Privoxy down. Setting a fixed -# hostname works around the problem. -# -# In other circumstances it might be desirable to show a -# hostname other than the one returned by the operating system. -# For example if the system has several different hostnames and -# you don't want to use the first one. -# -# Note that Privoxy does not validate the specified hostname -# value. -# -#hostname hostname.example.org -# -# 4. ACCESS CONTROL AND SECURITY -# =============================== -# -# This section of the config file controls the security-relevant -# aspects of Privoxy's configuration. -# -# -# 4.1. listen-address -# ==================== -# -# Specifies: -# -# The address and TCP port on which Privoxy will listen for -# client requests. -# -# Type of value: -# -# [IP-Address]:Port -# -# [Hostname]:Port -# -# Default value: -# -# 127.0.0.1:8118 -# -# Effect if unset: -# -# Bind to 127.0.0.1 (IPv4 localhost), port 8118. This is -# suitable and recommended for home users who run Privoxy on the -# same machine as their browser. -# -# Notes: -# -# You will need to configure your browser(s) to this proxy -# address and port. -# -# If you already have another service running on port 8118, or -# if you want to serve requests from other machines (e.g. on -# your local network) as well, you will need to override the -# default. -# -# You can use this statement multiple times to make Privoxy -# listen on more ports or more IP addresses. Suitable if your -# operating system does not support sharing IPv6 and IPv4 -# protocols on the same socket. -# -# If a hostname is used instead of an IP address, Privoxy will -# try to resolve it to an IP address and if there are multiple, -# use the first one returned. -# -# If the address for the hostname isn't already known on the -# system (for example because it's in /etc/hostname), this may -# result in DNS traffic. -# -# If the specified address isn't available on the system, or if -# the hostname can't be resolved, Privoxy will fail to start. -# -# IPv6 addresses containing colons have to be quoted by -# brackets. They can only be used if Privoxy has been compiled -# with IPv6 support. If you aren't sure if your version supports -# it, have a look at http://config.privoxy.org/show-status. -# -# Some operating systems will prefer IPv6 to IPv4 addresses even -# if the system has no IPv6 connectivity which is usually not -# expected by the user. Some even rely on DNS to resolve -# localhost which mean the "localhost" address used may not -# actually be local. -# -# It is therefore recommended to explicitly configure the -# intended IP address instead of relying on the operating -# system, unless there's a strong reason not to. -# -# If you leave out the address, Privoxy will bind to all IPv4 -# interfaces (addresses) on your machine and may become -# reachable from the Internet and/or the local network. Be aware -# that some GNU/Linux distributions modify that behaviour -# without updating the documentation. Check for non-standard -# patches if your Privoxy version behaves differently. -# -# If you configure Privoxy to be reachable from the network, -# consider using access control lists (ACL's, see below), and/or -# a firewall. -# -# If you open Privoxy to untrusted users, you will also want to -# make sure that the following actions are disabled: -# enable-edit-actions and enable-remote-toggle -# -# Example: -# -# Suppose you are running Privoxy on a machine which has the -# address 192.168.0.1 on your local private network -# (192.168.0.0) and has another outside connection with a -# different address. You want it to serve requests from inside -# only: -# -# listen-address 192.168.0.1:8118 -# -# Suppose you are running Privoxy on an IPv6-capable machine and -# you want it to listen on the IPv6 address of the loopback -# device: -# -# listen-address [::1]:8118 -# -# -listen-address 172.16.0.1:8118 -# -# 4.2. toggle -# ============ -# -# Specifies: -# -# Initial state of "toggle" status -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 1 -# -# Effect if unset: -# -# Act as if toggled on -# -# Notes: -# -# If set to 0, Privoxy will start in "toggled off" mode, i.e. -# mostly behave like a normal, content-neutral proxy with both -# ad blocking and content filtering disabled. See -# enable-remote-toggle below. -# -toggle 1 -# -# 4.3. enable-remote-toggle -# ========================== -# -# Specifies: -# -# Whether or not the web-based toggle feature may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based toggle feature is disabled. -# -# Notes: -# -# When toggled off, Privoxy mostly acts like a normal, -# content-neutral proxy, i.e. doesn't block ads or filter -# content. -# -# Access to the toggle feature can not be controlled separately -# by "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# toggle it for all users. So this option is not recommended for -# multi-user environments with untrusted users. -# -# Note that malicious client side code (e.g Java) is also -# capable of using this option. -# -# As a lot of Privoxy users don't read documentation, this -# feature is disabled by default. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-remote-toggle 0 -# -# 4.4. enable-remote-http-toggle -# =============================== -# -# Specifies: -# -# Whether or not Privoxy recognizes special HTTP headers to -# change its behaviour. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores special HTTP headers. -# -# Notes: -# -# When toggled on, the client can change Privoxy's behaviour by -# setting special HTTP headers. Currently the only supported -# special header is "X-Filter: No", to disable filtering for the -# ongoing request, even if it is enabled in one of the action -# files. -# -# This feature is disabled by default. If you are using Privoxy -# in a environment with trusted clients, you may enable this -# feature at your discretion. Note that malicious client side -# code (e.g Java) is also capable of using this feature. -# -# This option will be removed in future releases as it has been -# obsoleted by the more general header taggers. -# -enable-remote-http-toggle 0 -# -# 4.5. enable-edit-actions -# ========================= -# -# Specifies: -# -# Whether or not the web-based actions file editor may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based actions file editor is disabled. -# -# Notes: -# -# Access to the editor can not be controlled separately by -# "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# modify its configuration for all users. -# -# This option is not recommended for environments with untrusted -# users and as a lot of Privoxy users don't read documentation, -# this feature is disabled by default. -# -# Note that malicious client side code (e.g Java) is also -# capable of using the actions editor and you shouldn't enable -# this options unless you understand the consequences and are -# sure your browser is configured correctly. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-edit-actions 0 -# -# 4.6. enforce-blocks -# ==================== -# -# Specifies: -# -# Whether the user is allowed to ignore blocks and can "go there -# anyway". -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Blocks are not enforced. -# -# Notes: -# -# Privoxy is mainly used to block and filter requests as a -# service to the user, for example to block ads and other junk -# that clogs the pipes. Privoxy's configuration isn't perfect -# and sometimes innocent pages are blocked. In this situation it -# makes sense to allow the user to enforce the request and have -# Privoxy ignore the block. -# -# In the default configuration Privoxy's "Blocked" page contains -# a "go there anyway" link to adds a special string (the force -# prefix) to the request URL. If that link is used, Privoxy will -# detect the force prefix, remove it again and let the request -# pass. -# -# Of course Privoxy can also be used to enforce a network -# policy. In that case the user obviously should not be able to -# bypass any blocks, and that's what the "enforce-blocks" option -# is for. If it's enabled, Privoxy hides the "go there anyway" -# link. If the user adds the force prefix by hand, it will not -# be accepted and the circumvention attempt is logged. -# -# Examples: -# -# enforce-blocks 1 -# -enforce-blocks 0 -# -# 4.7. ACLs: permit-access and deny-access -# ========================================= -# -# Specifies: -# -# Who can access what. -# -# Type of value: -# -# src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]] -# -# Where src_addr and dst_addr are IPv4 addresses in dotted -# decimal notation or valid DNS names, port is a port number, -# and src_masklen and dst_masklen are subnet masks in CIDR -# notation, i.e. integer values from 2 to 30 representing the -# length (in bits) of the network address. The masks and the -# whole destination part are optional. -# -# If your system implements RFC 3493, then src_addr and dst_addr -# can be IPv6 addresses delimeted by brackets, port can be a -# number or a service name, and src_masklen and dst_masklen can -# be a number from 0 to 128. -# -# Default value: -# -# Unset -# -# If no port is specified, any port will match. If no -# src_masklen or src_masklen is given, the complete IP address -# has to match (i.e. 32 bits for IPv4 and 128 bits for IPv6). -# -# Effect if unset: -# -# Don't restrict access further than implied by listen-address -# -# Notes: -# -# Access controls are included at the request of ISPs and -# systems administrators, and are not usually needed by -# individual users. For a typical home user, it will normally -# suffice to ensure that Privoxy only listens on the localhost -# (127.0.0.1) or internal (home) network address by means of the -# listen-address option. -# -# Please see the warnings in the FAQ that Privoxy is not -# intended to be a substitute for a firewall or to encourage -# anyone to defer addressing basic security weaknesses. -# -# Multiple ACL lines are OK. If any ACLs are specified, Privoxy -# only talks to IP addresses that match at least one -# permit-access line and don't match any subsequent deny-access -# line. In other words, the last match wins, with the default -# being deny-access. -# -# If Privoxy is using a forwarder (see forward below) for a -# particular destination URL, the dst_addr that is examined is -# the address of the forwarder and NOT the address of the -# ultimate target. This is necessary because it may be -# impossible for the local Privoxy to determine the IP address -# of the ultimate target (that's often what gateways are used -# for). -# -# You should prefer using IP addresses over DNS names, because -# the address lookups take time. All DNS names must resolve! You -# can not use domain patterns like "*.org" or partial domain -# names. If a DNS name resolves to multiple IP addresses, only -# the first one is used. -# -# Some systems allow IPv4 clients to connect to IPv6 server -# sockets. Then the client's IPv4 address will be translated by -# the system into IPv6 address space with special prefix -# ::ffff:0:0/96 (so called IPv4 mapped IPv6 address). Privoxy -# can handle it and maps such ACL addresses automatically. -# -# Denying access to particular sites by ACL may have undesired -# side effects if the site in question is hosted on a machine -# which also hosts other sites (most sites are). -# -# Examples: -# -# Explicitly define the default behavior if no ACL and -# listen-address are set: "localhost" is OK. The absence of a -# dst_addr implies that all destination addresses are OK: -# -# permit-access localhost -# -# Allow any host on the same class C subnet as www.privoxy.org -# access to nothing but www.example.com (or other domains hosted -# on the same system): -# -# permit-access www.privoxy.org/24 www.example.com/32 -# -# Allow access from any host on the 26-bit subnet 192.168.45.64 -# to anywhere, with the exception that 192.168.45.73 may not -# access the IP address behind www.dirty-stuff.example.com: -# -# permit-access 192.168.45.64/26 -# deny-access 192.168.45.73 www.dirty-stuff.example.com -# -# Allow access from the IPv4 network 192.0.2.0/24 even if -# listening on an IPv6 wild card address (not supported on all -# platforms): -# -# permit-access 192.0.2.0/24 -# -# This is equivalent to the following line even if listening on -# an IPv4 address (not supported on all platforms): -# -# permit-access [::ffff:192.0.2.0]/120 -# -# -# 4.8. buffer-limit -# ================== -# -# Specifies: -# -# Maximum size of the buffer for content filtering. -# -# Type of value: -# -# Size in Kbytes -# -# Default value: -# -# 4096 -# -# Effect if unset: -# -# Use a 4MB (4096 KB) limit. -# -# Notes: -# -# For content filtering, i.e. the +filter and +deanimate-gif -# actions, it is necessary that Privoxy buffers the entire -# document body. This can be potentially dangerous, since a -# server could just keep sending data indefinitely and wait for -# your RAM to exhaust -- with nasty consequences. Hence this -# option. -# -# When a document buffer size reaches the buffer-limit, it is -# flushed to the client unfiltered and no further attempt to -# filter the rest of the document is made. Remember that there -# may be multiple threads running, which might require up to -# buffer-limit Kbytes each, unless you have enabled -# "single-threaded" above. -# -buffer-limit 4096 -# -# 4.9. enable-proxy-authentication-forwarding -# ============================================ -# -# Specifies: -# -# Whether or not proxy authentication through Privoxy should -# work. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Proxy authentication headers are removed. -# -# Notes: -# -# Privoxy itself does not support proxy authentication, but can -# allow clients to authenticate against Privoxy's parent proxy. -# -# By default Privoxy (3.0.21 and later) don't do that and remove -# Proxy-Authorization headers in requests and Proxy-Authenticate -# headers in responses to make it harder for malicious sites to -# trick inexperienced users into providing login information. -# -# If this option is enabled the headers are forwarded. -# -# Enabling this option is not recommended if there is no parent -# proxy that requires authentication or if the local network -# between Privoxy and the parent proxy isn't trustworthy. If -# proxy authentication is only required for some requests, it is -# recommended to use a client header filter to remove the -# authentication headers for requests where they aren't needed. -# -enable-proxy-authentication-forwarding 0 -# -# 5. FORWARDING -# ============== -# -# This feature allows routing of HTTP requests through a chain of -# multiple proxies. -# -# Forwarding can be used to chain Privoxy with a caching proxy to -# speed up browsing. Using a parent proxy may also be necessary if -# the machine that Privoxy runs on has no direct Internet access. -# -# Note that parent proxies can severely decrease your privacy level. -# For example a parent proxy could add your IP address to the -# request headers and if it's a caching proxy it may add the "Etag" -# header to revalidation requests again, even though you configured -# Privoxy to remove it. It may also ignore Privoxy's header time -# randomization and use the original values which could be used by -# the server as cookie replacement to track your steps between -# visits. -# -# Also specified here are SOCKS proxies. Privoxy supports the SOCKS -# 4 and SOCKS 4A protocols. -# -# -# 5.1. forward -# ============= -# -# Specifies: -# -# To which parent HTTP proxy specific requests should be routed. -# -# Type of value: -# -# target_pattern http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent[:port] is the DNS name or IP -# address of the parent HTTP proxy through which the requests -# should be forwarded, optionally followed by its listening port -# (default: 8000). Use a single dot (.) to denote "no -# forwarding". -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use parent HTTP proxies. -# -# Notes: -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made directly to the web servers. -# -# http_parent can be a numerical IPv6 address (if RFC 3493 is -# implemented). To prevent clashes with the port delimiter, the -# whole IP address has to be put into brackets. On the other -# hand a target_pattern containing an IPv6 address has to be put -# into angle brackets (normal brackets are reserved for regular -# expressions already). -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# Examples: -# -# Everything goes to an example parent proxy, except SSL on port -# 443 (which it doesn't handle): -# -# forward / parent-proxy.example.org:8080 -# forward :443 . -# -# Everything goes to our example ISP's caching proxy, except for -# requests to that ISP's sites: -# -# forward / caching-proxy.isp.example.net:8000 -# forward .isp.example.net . -# -# Parent proxy specified by an IPv6 address: -# -# forward / [2001:DB8::1]:8000 -# -# Suppose your parent proxy doesn't support IPv6: -# -# forward / parent-proxy.example.org:8000 -# forward ipv6-server.example.org . -# forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . -forward / 172.16.0.1:8080 -forward :443 . -# -# -# 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t -# ========================================================================= -# -# Specifies: -# -# Through which SOCKS proxy (and optionally to which parent HTTP -# proxy) specific requests should be routed. -# -# Type of value: -# -# target_pattern socks_proxy[:port] http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent and socks_proxy are IP -# addresses in dotted decimal notation or valid DNS names ( -# http_parent may be "." to denote "no HTTP forwarding"), and -# the optional port parameters are TCP ports, i.e. integer -# values from 1 to 65535 -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use SOCKS proxies. -# -# Notes: -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# The difference between forward-socks4 and forward-socks4a is -# that in the SOCKS 4A protocol, the DNS resolution of the -# target hostname happens on the SOCKS server, while in SOCKS 4 -# it happens locally. -# -# With forward-socks5 the DNS resolution will happen on the -# remote server as well. -# -# forward-socks5t works like vanilla forward-socks5 but lets -# Privoxy additionally use Tor-specific SOCKS extensions. -# Currently the only supported SOCKS extension is optimistic -# data which can reduce the latency for the first request made -# on a newly created connection. -# -# socks_proxy and http_parent can be a numerical IPv6 address -# (if RFC 3493 is implemented). To prevent clashes with the port -# delimiter, the whole IP address has to be put into brackets. -# On the other hand a target_pattern containing an IPv6 address -# has to be put into angle brackets (normal brackets are -# reserved for regular expressions already). -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made (HTTP-wise) directly to the -# web servers, albeit through a SOCKS proxy. -# -# Examples: -# -# From the company example.com, direct connections are made to -# all "internal" domains, but everything outbound goes through -# their ISP's proxy by way of example.com's corporate SOCKS 4A -# gateway to the Internet. -# -# forward-socks4a / socks-gw.example.com:1080 www-cache.isp.example.net:8080 -# forward .example.com . -# -# A rule that uses a SOCKS 4 gateway for all destinations but no -# HTTP parent looks like this: -# -# forward-socks4 / socks-gw.example.com:1080 . -# -# To chain Privoxy and Tor, both running on the same system, you -# would use something like: -# -# forward-socks5t / 127.0.0.1:9050 . -# -# Note that if you got Tor through one of the bundles, you may -# have to change the port from 9050 to 9150 (or even another -# one). For details, please check the documentation on the Tor -# website. -# -# The public Tor network can't be used to reach your local -# network, if you need to access local servers you therefore -# might want to make some exceptions: -# -# forward 192.168.*.*/ . -# forward 10.*.*.*/ . -# forward 127.*.*.*/ . -# -# Unencrypted connections to systems in these address ranges -# will be as (un)secure as the local network is, but the -# alternative is that you can't reach the local network through -# Privoxy at all. Of course this may actually be desired and -# there is no reason to make these exceptions if you aren't sure -# you need them. -# -# If you also want to be able to reach servers in your local -# network by using their names, you will need additional -# exceptions that look like this: -# -# forward localhost/ . -# -# -# 5.3. forwarded-connect-retries -# =============================== -# -# Specifies: -# -# How often Privoxy retries if a forwarded connection request -# fails. -# -# Type of value: -# -# Number of retries. -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Connections forwarded through other proxies are treated like -# direct connections and no retry attempts are made. -# -# Notes: -# -# forwarded-connect-retries is mainly interesting for socks4a -# connections, where Privoxy can't detect why the connections -# failed. The connection might have failed because of a DNS -# timeout in which case a retry makes sense, but it might also -# have failed because the server doesn't exist or isn't -# reachable. In this case the retry will just delay the -# appearance of Privoxy's error message. -# -# Note that in the context of this option, "forwarded -# connections" includes all connections that Privoxy forwards -# through other proxies. This option is not limited to the HTTP -# CONNECT method. -# -# Only use this option, if you are getting lots of -# forwarding-related error messages that go away when you try -# again manually. Start with a small value and check Privoxy's -# logfile from time to time, to see how many retries are usually -# needed. -# -# Examples: -# -# forwarded-connect-retries 1 -# -forwarded-connect-retries 0 -# -# 6. MISCELLANEOUS -# ================= -# -# 6.1. accept-intercepted-requests -# ================================= -# -# Specifies: -# -# Whether intercepted requests should be treated as valid. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Only proxy requests are accepted, intercepted requests are -# treated as invalid. -# -# Notes: -# -# If you don't trust your clients and want to force them to use -# Privoxy, enable this option and configure your packet filter -# to redirect outgoing HTTP connections into Privoxy. -# -# Note that intercepting encrypted connections (HTTPS) isn't -# supported. -# -# Make sure that Privoxy's own requests aren't redirected as -# well. Additionally take care that Privoxy can't intentionally -# connect to itself, otherwise you could run into redirection -# loops if Privoxy's listening port is reachable by the outside -# or an attacker has access to the pages you visit. -# -# Examples: -# -# accept-intercepted-requests 1 -# -accept-intercepted-requests 0 -# -# 6.2. allow-cgi-request-crunching -# ================================= -# -# Specifies: -# -# Whether requests to Privoxy's CGI pages can be blocked or -# redirected. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores block and redirect actions for its CGI pages. -# -# Notes: -# -# By default Privoxy ignores block or redirect actions for its -# CGI pages. Intercepting these requests can be useful in -# multi-user setups to implement fine-grained access control, -# but it can also render the complete web interface useless and -# make debugging problems painful if done without care. -# -# Don't enable this option unless you're sure that you really -# need it. -# -# Examples: -# -# allow-cgi-request-crunching 1 -# -allow-cgi-request-crunching 0 -# -# 6.3. split-large-forms -# ======================= -# -# Specifies: -# -# Whether the CGI interface should stay compatible with broken -# HTTP clients. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The CGI form generate long GET URLs. -# -# Notes: -# -# Privoxy's CGI forms can lead to rather long URLs. This isn't a -# problem as far as the HTTP standard is concerned, but it can -# confuse clients with arbitrary URL length limitations. -# -# Enabling split-large-forms causes Privoxy to divide big forms -# into smaller ones to keep the URL length down. It makes -# editing a lot less convenient and you can no longer submit all -# changes at once, but at least it works around this browser -# bug. -# -# If you don't notice any editing problems, there is no reason -# to enable this option, but if one of the submit buttons -# appears to be broken, you should give it a try. -# -# Examples: -# -# split-large-forms 1 -# -split-large-forms 0 -# -# 6.4. keep-alive-timeout -# ======================== -# -# Specifies: -# -# Number of seconds after which an open connection will no -# longer be reused. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not kept alive. -# -# Notes: -# -# This option allows clients to keep the connection to Privoxy -# alive. If the server supports it, Privoxy will keep the -# connection to the server alive as well. Under certain -# circumstances this may result in speed-ups. -# -# By default, Privoxy will close the connection to the server if -# the client connection gets closed, or if the specified timeout -# has been reached without a new request coming in. This -# behaviour can be changed with the connection-sharing option. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Note that a timeout of five seconds as used in the default -# configuration file significantly decreases the number of -# connections that will be reused. The value is used because -# some browsers limit the number of connections they open to a -# single host and apply the same limit to proxies. This can -# result in a single website "grabbing" all the connections the -# browser allows, which means connections to other websites -# can't be opened until the connections currently in use time -# out. -# -# Several users have reported this as a Privoxy bug, so the -# default value has been reduced. Consider increasing it to 300 -# seconds or even more if you think your browser can handle it. -# If your browser appears to be hanging, it probably can't. -# -# Examples: -# -# keep-alive-timeout 300 -# -keep-alive-timeout 5 -# -# 6.5. tolerate-pipelining -# ========================= -# -# Specifies: -# -# Whether or not pipelined requests should be served. -# -# Type of value: -# -# 0 or 1. -# -# Default value: -# -# None -# -# Effect if unset: -# -# If Privoxy receives more than one request at once, it -# terminates the client connection after serving the first one. -# -# Notes: -# -# Privoxy currently doesn't pipeline outgoing requests, thus -# allowing pipelining on the client connection is not guaranteed -# to improve the performance. -# -# By default Privoxy tries to discourage clients from pipelining -# by discarding aggressively pipelined requests, which forces -# the client to resend them through a new connection. -# -# This option lets Privoxy tolerate pipelining. Whether or not -# that improves performance mainly depends on the client -# configuration. -# -# If you are seeing problems with pages not properly loading, -# disabling this option could work around the problem. -# -# Examples: -# -# tolerate-pipelining 1 -# -tolerate-pipelining 1 -# -# 6.6. default-server-timeout -# ============================ -# -# Specifies: -# -# Assumed server-side keep-alive timeout if not specified by the -# server. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections for which the server didn't specify the keep-alive -# timeout are not reused. -# -# Notes: -# -# Enabling this option significantly increases the number of -# connections that are reused, provided the keep-alive-timeout -# option is also enabled. -# -# While it also increases the number of connections problems -# when Privoxy tries to reuse a connection that already has been -# closed on the server side, or is closed while Privoxy is -# trying to reuse it, this should only be a problem if it -# happens for the first request sent by the client. If it -# happens for requests on reused client connections, Privoxy -# will simply close the connection and the client is supposed to -# retry the request without bothering the user. -# -# Enabling this option is therefore only recommended if the -# connection-sharing option is disabled. -# -# It is an error to specify a value larger than the -# keep-alive-timeout value. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Examples: -# -# default-server-timeout 60 -# -#default-server-timeout 60 -# -# 6.7. connection-sharing -# ======================== -# -# Specifies: -# -# Whether or not outgoing connections that have been kept alive -# should be shared between different incoming connections. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not shared. -# -# Notes: -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support, or if it's disabled. -# -# Notes: -# -# Note that reusing connections doesn't necessary cause -# speedups. There are also a few privacy implications you should -# be aware of. -# -# If this option is effective, outgoing connections are shared -# between clients (if there are more than one) and closing the -# browser that initiated the outgoing connection does no longer -# affect the connection between Privoxy and the server unless -# the client's request hasn't been completed yet. -# -# If the outgoing connection is idle, it will not be closed -# until either Privoxy's or the server's timeout is reached. -# While it's open, the server knows that the system running -# Privoxy is still there. -# -# If there are more than one client (maybe even belonging to -# multiple users), they will be able to reuse each others -# connections. This is potentially dangerous in case of -# authentication schemes like NTLM where only the connection is -# authenticated, instead of requiring authentication for each -# request. -# -# If there is only a single client, and if said client can keep -# connections alive on its own, enabling this option has next to -# no effect. If the client doesn't support connection -# keep-alive, enabling this option may make sense as it allows -# Privoxy to keep outgoing connections alive even if the client -# itself doesn't support it. -# -# You should also be aware that enabling this option increases -# the likelihood of getting the "No server or forwarder data" -# error message, especially if you are using a slow connection -# to the Internet. -# -# This option should only be used by experienced users who -# understand the risks and can weight them against the benefits. -# -# Examples: -# -# connection-sharing 1 -# -#connection-sharing 1 -# -# 6.8. socket-timeout -# ==================== -# -# Specifies: -# -# Number of seconds after which a socket times out if no data is -# received. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# A default value of 300 seconds is used. -# -# Notes: -# -# The default is quite high and you probably want to reduce it. -# If you aren't using an occasionally slow proxy like Tor, -# reducing it to a few seconds should be fine. -# -# Examples: -# -# socket-timeout 300 -# -socket-timeout 300 -# -# 6.9. max-client-connections -# ============================ -# -# Specifies: -# -# Maximum number of client connections that will be served. -# -# Type of value: -# -# Positive number. -# -# Default value: -# -# 128 -# -# Effect if unset: -# -# Connections are served until a resource limit is reached. -# -# Notes: -# -# Privoxy creates one thread (or process) for every incoming -# client connection that isn't rejected based on the access -# control settings. -# -# If the system is powerful enough, Privoxy can theoretically -# deal with several hundred (or thousand) connections at the -# same time, but some operating systems enforce resource limits -# by shutting down offending processes and their default limits -# may be below the ones Privoxy would require under heavy load. -# -# Configuring Privoxy to enforce a connection limit below the -# thread or process limit used by the operating system makes -# sure this doesn't happen. Simply increasing the operating -# system's limit would work too, but if Privoxy isn't the only -# application running on the system, you may actually want to -# limit the resources used by Privoxy. -# -# If Privoxy is only used by a single trusted user, limiting the -# number of client connections is probably unnecessary. If there -# are multiple possibly untrusted users you probably still want -# to additionally use a packet filter to limit the maximal -# number of incoming connections per client. Otherwise a -# malicious user could intentionally create a high number of -# connections to prevent other users from using Privoxy. -# -# Obviously using this option only makes sense if you choose a -# limit below the one enforced by the operating system. -# -# One most POSIX-compliant systems Privoxy can't properly deal -# with more than FD_SETSIZE file descriptors at the same time -# and has to reject connections if the limit is reached. This -# will likely change in a future version, but currently this -# limit can't be increased without recompiling Privoxy with a -# different FD_SETSIZE limit. -# -# Examples: -# -# max-client-connections 256 -# -#max-client-connections 256 -# -# 6.10. handle-as-empty-doc-returns-ok -# ===================================== -# -# Specifies: -# -# The status code Privoxy returns for pages blocked with -# +handle-as-empty-document. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy returns a status 403(forbidden) for all blocked pages. -# -# Effect if set: -# -# Privoxy returns a status 200(OK) for pages blocked with -# +handle-as-empty-document and a status 403(Forbidden) for all -# other blocked pages. -# -# Notes: -# -# This directive was added as a work-around for Firefox bug -# 492459: "Websites are no longer rendered if SSL requests for -# JavaScripts are blocked by a proxy." -# (https://bugzilla.mozilla.org/show_bug.cgi?id=492459), the bug -# has been fixed for quite some time, but this directive is also -# useful to make it harder for websites to detect whether or not -# resources are being blocked. -# -#handle-as-empty-doc-returns-ok 1 -# -# 6.11. enable-compression -# ========================= -# -# Specifies: -# -# Whether or not buffered content is compressed before delivery. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy does not compress buffered content. -# -# Effect if set: -# -# Privoxy compresses buffered content before delivering it to -# the client, provided the client supports it. -# -# Notes: -# -# This directive is only supported if Privoxy has been compiled -# with FEATURE_COMPRESSION, which should not to be confused with -# FEATURE_ZLIB. -# -# Compressing buffered content is mainly useful if Privoxy and -# the client are running on different systems. If they are -# running on the same system, enabling compression is likely to -# slow things down. If you didn't measure otherwise, you should -# assume that it does and keep this option disabled. -# -# Privoxy will not compress buffered content below a certain -# length. -# -#enable-compression 1 -# -# 6.12. compression-level -# ======================== -# -# Specifies: -# -# The compression level that is passed to the zlib library when -# compressing buffered content. -# -# Type of value: -# -# Positive number ranging from 0 to 9. -# -# Default value: -# -# 1 -# -# Notes: -# -# Compressing the data more takes usually longer than -# compressing it less or not compressing it at all. Which level -# is best depends on the connection between Privoxy and the -# client. If you can't be bothered to benchmark it for yourself, -# you should stick with the default and keep compression -# disabled. -# -# If compression is disabled, the compression level is -# irrelevant. -# -# Examples: -# -# # Best speed (compared to the other levels) -# compression-level 1 -# -# # Best compression -# compression-level 9 -# -# # No compression. Only useful for testing as the added header -# # slightly increases the amount of data that has to be sent. -# # If your benchmark shows that using this compression level -# # is superior to using no compression at all, the benchmark -# # is likely to be flawed. -# compression-level 0 -# -# -#compression-level 1 -# -# 6.13. client-header-order -# ========================== -# -# Specifies: -# -# The order in which client headers are sorted before forwarding -# them. -# -# Type of value: -# -# Client header names delimited by spaces or tabs -# -# Default value: -# -# None -# -# Notes: -# -# By default Privoxy leaves the client headers in the order they -# were sent by the client. Headers are modified in-place, new -# headers are added at the end of the already existing headers. -# -# The header order can be used to fingerprint client requests -# independently of other headers like the User-Agent. -# -# This directive allows to sort the headers differently to -# better mimic a different User-Agent. Client headers will be -# emitted in the order given, headers whose name isn't -# explicitly specified are added at the end. -# -# Note that sorting headers in an uncommon way will make -# fingerprinting actually easier. Encrypted headers are not -# affected by this directive. -# -#client-header-order Host \ -# Accept \ -# Accept-Language \ -# Accept-Encoding \ -# Proxy-Connection \ -# Referer \ -# Cookie \ -# DNT \ -# If-Modified-Since \ -# Cache-Control \ -# Content-Length \ -# Content-Type -# -# -# 7. WINDOWS GUI OPTIONS -# ======================= -# -# Privoxy has a number of options specific to the Windows GUI -# interface: -# -# -# -# If "activity-animation" is set to 1, the Privoxy icon will animate -# when "Privoxy" is active. To turn off, set to 0. -# -#activity-animation 1 -# -# -# -# If "log-messages" is set to 1, Privoxy copies log messages to the -# console window. The log detail depends on the debug directive. -# -#log-messages 1 -# -# -# -# If "log-buffer-size" is set to 1, the size of the log buffer, i.e. -# the amount of memory used for the log messages displayed in the -# console window, will be limited to "log-max-lines" (see below). -# -# Warning: Setting this to 0 will result in the buffer to grow -# infinitely and eat up all your memory! -# -#log-buffer-size 1 -# -# -# -# log-max-lines is the maximum number of lines held in the log -# buffer. See above. -# -#log-max-lines 200 -# -# -# -# If "log-highlight-messages" is set to 1, Privoxy will highlight -# portions of the log messages with a bold-faced font: -# -#log-highlight-messages 1 -# -# -# -# The font used in the console window: -# -#log-font-name Comic Sans MS -# -# -# -# Font size used in the console window: -# -#log-font-size 8 -# -# -# -# "show-on-task-bar" controls whether or not Privoxy will appear as -# a button on the Task bar when minimized: -# -#show-on-task-bar 0 -# -# -# -# If "close-button-minimizes" is set to 1, the Windows close button -# will minimize Privoxy instead of closing the program (close with -# the exit option on the File menu). -# -#close-button-minimizes 1 -# -# -# -# The "hide-console" option is specific to the MS-Win console -# version of Privoxy. If this option is used, Privoxy will -# disconnect from and hide the command console. -# -#hide-console -# -# -# diff --git a/templates/rsyslog.conf.j2 b/templates/rsyslog.conf.j2 deleted file mode 100644 index 25513801..00000000 --- a/templates/rsyslog.conf.j2 +++ /dev/null @@ -1,61 +0,0 @@ -# /etc/rsyslog.conf Configuration file for rsyslog. -# -# For more information see -# /usr/share/doc/rsyslog-doc/html/rsyslog_conf.html -# -# Default logging rules can be found in /etc/rsyslog.d/50-default.conf - -# -################# -#### MODULES #### -################# - -module(load="imuxsock") # provides support for local system logging -module(load="imklog") # provides kernel logging support -#module(load="immark") # provides --MARK-- message capability - -# provides UDP syslog reception -#module(load="imudp") -#input(type="imudp" port="514") - -# provides TCP syslog reception -#module(load="imtcp") -#input(type="imtcp" port="514") - -# Enable non-kernel facility klog messages -$KLogPermitNonKernelFacility on - -########################### -#### GLOBAL DIRECTIVES #### -########################### - -# -# Use traditional timestamp format. -# To enable high precision timestamps, comment out the following line. -# -$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat - -# Filter duplicated messages -$RepeatedMsgReduction on - -# -# Set the default permissions for all log files. -# -$FileOwner syslog -$FileGroup adm -$FileCreateMode 0640 -$DirCreateMode 0755 -$Umask 0022 -$PrivDropToUser syslog -$PrivDropToGroup syslog - -# -# Where to place spool and state files -# -$WorkDirectory /var/spool/rsyslog - -# -# Include all config files in /etc/rsyslog.d/ -# -$IncludeConfig /etc/rsyslog.d/*.conf - diff --git a/templates/usr.sbin.dnsmasq.j2 b/templates/usr.sbin.dnsmasq.j2 deleted file mode 100644 index 9b2c34bd..00000000 --- a/templates/usr.sbin.dnsmasq.j2 +++ /dev/null @@ -1,68 +0,0 @@ -# ------------------------------------------------------------------ -# -# Copyright (C) 2009 John Dong -# Copyright (C) 2010 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License published by the Free Software Foundation. -# -# ------------------------------------------------------------------ - -@{TFTP_DIR}=/var/tftp /srv/tftpboot - -#include - -/usr/sbin/dnsmasq { - #include - #include - #include - - capability net_bind_service, - capability setgid, - capability setuid, - capability dac_override, - capability net_admin, # for DHCP server - capability net_raw, # for DHCP server ping checks - network inet raw, - - signal (receive) peer=/usr/sbin/libvirtd, - ptrace (readby) peer=/usr/sbin/libvirtd, - - /etc/dnsmasq.conf r, - /etc/dnsmasq.d/ r, - /etc/dnsmasq.d/* r, - /etc/ethers r, - /etc/NetworkManager/dnsmasq.d/ r, - /etc/NetworkManager/dnsmasq.d/* r, - /etc/block.hosts r, - - /usr/sbin/dnsmasq mr, - - /{,var/}run/*dnsmasq*.pid w, - /{,var/}run/dnsmasq-forwarders.conf r, - /{,var/}run/dnsmasq/ r, - /{,var/}run/dnsmasq/* rw, - - /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage - - # for the read-only TFTP server - @{TFTP_DIR}/ r, - @{TFTP_DIR}/** r, - - # libvirt config, lease and hosts files for dnsmasq - /var/lib/libvirt/dnsmasq/ r, - /var/lib/libvirt/dnsmasq/* r, - /var/lib/libvirt/dnsmasq/*.leases rw, - - # libvirt pid files for dnsmasq - /{,var/}run/libvirt/network/ r, - /{,var/}run/libvirt/network/*.pid rw, - - # NetworkManager integration - /{,var/}run/nm-dns-dnsmasq.conf r, - /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, - /{,var/}run/NetworkManager/dnsmasq.conf r, - /{,var/}run/NetworkManager/dnsmasq.pid w, - -} diff --git a/templates/usr.sbin.privoxy.j2 b/templates/usr.sbin.privoxy.j2 deleted file mode 100644 index 5f8d9ddf..00000000 --- a/templates/usr.sbin.privoxy.j2 +++ /dev/null @@ -1,15 +0,0 @@ -#include - -/usr/sbin/privoxy { - #include - #include - - capability setgid, - capability setuid, - - /etc/privoxy/* r, - /etc/privoxy/templates/* r, - /run/privoxy.pid w, - /var/log/privoxy/logfile w, - -} diff --git a/users.yml b/users.yml index f995cd45..e2060a4f 100644 --- a/users.yml +++ b/users.yml @@ -82,7 +82,7 @@ register: PayloadContentCA - name: Build the mobileconfigs - template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 + template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 with_together: - "{{ users }}" - "{{ PayloadContent.results }}" From 7a8d58783f6e92adec7fc2e0f18d462d1f821c1a Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 20:03:33 +0300 Subject: [PATCH 026/769] Roles and Google cloud --- roles/common/templates/10periodic.j2 | 4 + .../common/templates/50unattended-upgrades.j2 | 59 + roles/digitalocean/templates/20-ipv6.cfg.j2 | 6 + roles/features/templates/000-default.conf.j2 | 11 + .../templates/10-loopback-services.cfg.j2 | 9 + roles/features/templates/adblock.sh | 50 + roles/features/templates/dnsmasq.conf.j2 | 669 ++++++ roles/features/templates/pagespeed.conf.j2 | 369 +++ roles/features/templates/ports.conf.j2 | 13 + roles/features/templates/privoxy_config.j2 | 2107 +++++++++++++++++ roles/features/templates/usr.sbin.dnsmasq.j2 | 68 + roles/features/templates/usr.sbin.privoxy.j2 | 15 + roles/logging/templates/audit.rules.j2 | 101 + roles/logging/templates/auditd.conf.j2 | 32 + roles/security/templates/CIS.conf.j2 | 15 + roles/security/templates/rsyslog.conf.j2 | 61 + roles/vpn/templates/easy-rsa.vars.j2 | 198 ++ roles/vpn/templates/ipsec.conf.j2 | 34 + roles/vpn/templates/ipsec.secrets.j2 | 2 + roles/vpn/templates/mobileconfig.j2 | 144 ++ 20 files changed, 3967 insertions(+) create mode 100644 roles/common/templates/10periodic.j2 create mode 100644 roles/common/templates/50unattended-upgrades.j2 create mode 100644 roles/digitalocean/templates/20-ipv6.cfg.j2 create mode 100644 roles/features/templates/000-default.conf.j2 create mode 100644 roles/features/templates/10-loopback-services.cfg.j2 create mode 100644 roles/features/templates/adblock.sh create mode 100644 roles/features/templates/dnsmasq.conf.j2 create mode 100644 roles/features/templates/pagespeed.conf.j2 create mode 100644 roles/features/templates/ports.conf.j2 create mode 100644 roles/features/templates/privoxy_config.j2 create mode 100644 roles/features/templates/usr.sbin.dnsmasq.j2 create mode 100644 roles/features/templates/usr.sbin.privoxy.j2 create mode 100644 roles/logging/templates/audit.rules.j2 create mode 100644 roles/logging/templates/auditd.conf.j2 create mode 100644 roles/security/templates/CIS.conf.j2 create mode 100644 roles/security/templates/rsyslog.conf.j2 create mode 100644 roles/vpn/templates/easy-rsa.vars.j2 create mode 100644 roles/vpn/templates/ipsec.conf.j2 create mode 100644 roles/vpn/templates/ipsec.secrets.j2 create mode 100644 roles/vpn/templates/mobileconfig.j2 diff --git a/roles/common/templates/10periodic.j2 b/roles/common/templates/10periodic.j2 new file mode 100644 index 00000000..75870203 --- /dev/null +++ b/roles/common/templates/10periodic.j2 @@ -0,0 +1,4 @@ +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Download-Upgradeable-Packages "1"; +APT::Periodic::AutocleanInterval "7"; +APT::Periodic::Unattended-Upgrade "1"; \ No newline at end of file diff --git a/roles/common/templates/50unattended-upgrades.j2 b/roles/common/templates/50unattended-upgrades.j2 new file mode 100644 index 00000000..5f8fb159 --- /dev/null +++ b/roles/common/templates/50unattended-upgrades.j2 @@ -0,0 +1,59 @@ +// Automatically upgrade packages from these (origin:archive) pairs +Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}-security"; + "${distro_id}:${distro_codename}-updates"; +// "${distro_id}:${distro_codename}-proposed"; +// "${distro_id}:${distro_codename}-backports"; +}; + +// List of packages to not update (regexp are supported) +Unattended-Upgrade::Package-Blacklist { +// "vim"; +// "libc6"; +// "libc6-dev"; +// "libc6-i686"; +}; + +// This option allows you to control if on a unclean dpkg exit +// unattended-upgrades will automatically run +// dpkg --force-confold --configure -a +// The default is true, to ensure updates keep getting installed +//Unattended-Upgrade::AutoFixInterruptedDpkg "false"; + +// Split the upgrade into the smallest possible chunks so that +// they can be interrupted with SIGUSR1. This makes the upgrade +// a bit slower but it has the benefit that shutdown while a upgrade +// is running is possible (with a small delay) +//Unattended-Upgrade::MinimalSteps "true"; + +// Install all unattended-upgrades when the machine is shuting down +// instead of doing it in the background while the machine is running +// This will (obviously) make shutdown slower +//Unattended-Upgrade::InstallOnShutdown "true"; + +// Send email to this address for problems or packages upgrades +// If empty or unset then no email is sent, make sure that you +// have a working mail setup on your system. A package that provides +// 'mailx' must be installed. E.g. "user@example.com" +//Unattended-Upgrade::Mail "root"; + +// Set this value to "true" to get emails only on errors. Default +// is to always send a mail if Unattended-Upgrade::Mail is set +//Unattended-Upgrade::MailOnlyOnError "true"; + +// Do automatic removal of new unused dependencies after the upgrade +// (equivalent to apt-get autoremove) +//Unattended-Upgrade::Remove-Unused-Dependencies "false"; + +// Automatically reboot *WITHOUT CONFIRMATION* +// if the file /var/run/reboot-required is found after the upgrade +//Unattended-Upgrade::Automatic-Reboot "false"; + +// If automatic reboot is enabled and needed, reboot at the specific +// time instead of immediately +// Default: "now" +//Unattended-Upgrade::Automatic-Reboot-Time "02:00"; + +// Use apt bandwidth limit feature, this example limits the download +// speed to 70kb/sec +//Acquire::http::Dl-Limit "70"; diff --git a/roles/digitalocean/templates/20-ipv6.cfg.j2 b/roles/digitalocean/templates/20-ipv6.cfg.j2 new file mode 100644 index 00000000..7db27bbb --- /dev/null +++ b/roles/digitalocean/templates/20-ipv6.cfg.j2 @@ -0,0 +1,6 @@ +iface eth0 inet6 static + address {{ item.ip_address }} + netmask {{ item.netmask }} + gateway {{ item.gateway }} + autoconf 0 + dns-nameservers 2001:4860:4860::8844 2001:4860:4860::8888 diff --git a/roles/features/templates/000-default.conf.j2 b/roles/features/templates/000-default.conf.j2 new file mode 100644 index 00000000..7aa917b7 --- /dev/null +++ b/roles/features/templates/000-default.conf.j2 @@ -0,0 +1,11 @@ + + + Order deny,allow + Allow from all + + RewriteEngine On + RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] + ProxyPass / http://$1 + ProxyPassReverse / http://$1 + ProxyPreserveHost On + diff --git a/roles/features/templates/10-loopback-services.cfg.j2 b/roles/features/templates/10-loopback-services.cfg.j2 new file mode 100644 index 00000000..c5c47e47 --- /dev/null +++ b/roles/features/templates/10-loopback-services.cfg.j2 @@ -0,0 +1,9 @@ +auto lo:100 +iface lo:100 inet static + address 172.16.0.1 + netmask 255.255.255.255 + +iface lo:100 inet6 static + address FCAA::1 + netmask 64 + autoconf 0 diff --git a/roles/features/templates/adblock.sh b/roles/features/templates/adblock.sh new file mode 100644 index 00000000..a6a88581 --- /dev/null +++ b/roles/features/templates/adblock.sh @@ -0,0 +1,50 @@ +#!/bin/sh +#Block ads, malware, etc. + +# Redirect endpoint +ENDPOINT_IP4="0.0.0.0" +ENDPOINT_IP6="::" +IPV6="Y" + +#Delete the old block.hosts to make room for the updates +rm -f /etc/block.hosts + +echo 'Downloading hosts lists...' +#Download and process the files needed to make the lists (enable/add more, if you want) +wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > /tmp/block.build.list +wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list +wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list +wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list + +#Add black list, if non-empty +if [ -s "/etc/black.list" ] +then + echo 'Adding blacklist...' + awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /etc/black.list >> /tmp/block.build.list +fi + +#Sort the download/black lists +awk '{sub(/\r$/,"");print $1,$2}' /tmp/block.build.list|sort -u > /tmp/block.build.before + +#Filter (if applicable) +if [ -s "/etc/white.list" ] +then + #Filter the blacklist, supressing whitelist matches + # This is relatively slow =-( + echo 'Filtering white list...' + egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /etc/block.hosts +else + cat /tmp/block.build.before > /etc/block.hosts +fi + +if [ "$IPV6" = "Y" ] +then + safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') + safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') + echo 'Adding ipv6 support...' + sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /etc/block.hosts +fi + +service dnsmasq restart + +exit 0 diff --git a/roles/features/templates/dnsmasq.conf.j2 b/roles/features/templates/dnsmasq.conf.j2 new file mode 100644 index 00000000..d28cfac3 --- /dev/null +++ b/roles/features/templates/dnsmasq.conf.j2 @@ -0,0 +1,669 @@ +# Configuration file for dnsmasq. +# +# Format is one option per line, legal options are the same +# as the long options legal on the command line. See +# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. + +# Listen on this specific port instead of the standard DNS port +# (53). Setting this to zero completely disables DNS function, +# leaving only DHCP and/or TFTP. +#port=5353 + +# The following two options make you a better netizen, since they +# tell dnsmasq to filter out queries which the public DNS cannot +# answer, and which load the servers (especially the root servers) +# unnecessarily. If you have a dial-on-demand link they also stop +# these requests from bringing up the link unnecessarily. + +# Never forward plain names (without a dot or domain part) +#domain-needed +# Never forward addresses in the non-routed address spaces. +#bogus-priv + +# Uncomment these to enable DNSSEC validation and caching: +# (Requires dnsmasq to be built with DNSSEC option.) +#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf +#dnssec + +# Replies which are not DNSSEC signed may be legitimate, because the domain +# is unsigned, or may be forgeries. Setting this option tells dnsmasq to +# check that an unsigned reply is OK, by finding a secure proof that a DS +# record somewhere between the root and the domain does not exist. +# The cost of setting this is that even queries in unsigned domains will need +# one or more extra DNS queries to verify. +#dnssec-check-unsigned + +# Uncomment this to filter useless windows-originated DNS requests +# which can trigger dial-on-demand links needlessly. +# Note that (amongst other things) this blocks all SRV requests, +# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk. +# This option only affects forwarding, SRV records originating for +# dnsmasq (via srv-host= lines) are not suppressed by it. +#filterwin2k + +# Change this line if you want dns to get its upstream servers from +# somewhere other that /etc/resolv.conf +#resolv-file= + +# By default, dnsmasq will send queries to any of the upstream +# servers it knows about and tries to favour servers to are known +# to be up. Uncommenting this forces dnsmasq to try each query +# with each server strictly in the order they appear in +# /etc/resolv.conf +#strict-order + +# If you don't want dnsmasq to read /etc/resolv.conf or any other +# file, getting its servers from this file instead (see below), then +# uncomment this. +#no-resolv + +# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv +# files for changes and re-read them then uncomment this. +#no-poll + +# Add other name servers here, with domain specs if they are for +# non-public domains. +#server=/localnet/192.168.0.1 + +# Example of routing PTR queries to nameservers: this will send all +# address->name queries for 192.168.3/24 to nameserver 10.1.2.3 +#server=/3.168.192.in-addr.arpa/10.1.2.3 + +# Add local-only domains here, queries in these domains are answered +# from /etc/hosts or DHCP only. +#local=/localnet/ + +# Add domains which you want to force to an IP address here. +# The example below send any host in double-click.net to a local +# web-server. +#address=/double-click.net/127.0.0.1 + +# --address (and --server) work with IPv6 addresses too. +#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 + +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to the vpn and search ipsets: +#ipset=/yahoo.com/google.com/vpn,search + +# You can control how dnsmasq talks to a server: this forces +# queries to 10.1.2.3 to be routed via eth1 +# server=10.1.2.3@eth1 +server=8.8.8.8 +server=8.8.4.4 + +# and this sets the source (ie local) address used to talk to +# 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that +# IP on the machine, obviously). +# server=10.1.2.3@192.168.1.1#55 + +# If you want dnsmasq to change uid and gid to something other +# than the default, edit the following lines. +user=nobody +group=nogroup + +# If you want dnsmasq to listen for DHCP and DNS requests only on +# specified interfaces (and the loopback) give the name of the +# interface (eg eth0) here. +# Repeat the line for more than one interface. +#interface=lo +# Or you can specify which interface _not_ to listen on +#except-interface= +# Or which to listen on by address (remember to include 127.0.0.1 if +# you use this.) +listen-address=172.16.0.1,127.0.0.1,FCAA::1 +# If you want dnsmasq to provide only DNS service on an interface, +# configure it as shown above, and then use the following line to +# disable DHCP and TFTP on it. +#no-dhcp-interface= + +# On systems which support it, dnsmasq binds the wildcard address, +# even when it is listening on only some interfaces. It then discards +# requests that it shouldn't reply to. This has the advantage of +# working even when interfaces come and go and change address. If you +# want dnsmasq to really bind only the interfaces it is listening on, +# uncomment this option. About the only time you may need this is when +# running another nameserver on the same machine. +bind-interfaces + +# If you don't want dnsmasq to read /etc/hosts, uncomment the +# following line. +#no-hosts +# or if you want it to read another file, as well as /etc/hosts, use +# this. +addn-hosts=/etc/block.hosts + +# Set this (and domain: see below) if you want to have a domain +# automatically added to simple names in a hosts-file. +#expand-hosts + +# Set the domain for dnsmasq. this is optional, but if it is set, it +# does the following things. +# 1) Allows DHCP hosts to have fully qualified domain names, as long +# as the domain part matches this setting. +# 2) Sets the "domain" DHCP option thereby potentially setting the +# domain of all systems configured by DHCP +# 3) Provides the domain part for "expand-hosts" +#domain=thekelleys.org.uk + +# Set a different domain for a particular subnet +#domain=wireless.thekelleys.org.uk,192.168.2.0/24 + +# Same idea, but range rather then subnet +#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200 + +# Uncomment this to enable the integrated DHCP server, you need +# to supply the range of addresses available for lease and optionally +# a lease time. If you have more than one network, you will need to +# repeat this for each network on which you want to supply DHCP +# service. +#dhcp-range=192.168.0.50,192.168.0.150,12h + +# This is an example of a DHCP range where the netmask is given. This +# is needed for networks we reach the dnsmasq DHCP server via a relay +# agent. If you don't know what a DHCP relay agent is, you probably +# don't need to worry about this. +#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h + +# This is an example of a DHCP range which sets a tag, so that +# some DHCP options may be set only for this network. +#dhcp-range=set:red,192.168.0.50,192.168.0.150 + +# Use this DHCP range only when the tag "green" is set. +#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h + +# Specify a subnet which can't be used for dynamic address allocation, +# is available for hosts with matching --dhcp-host lines. Note that +# dhcp-host declarations will be ignored unless there is a dhcp-range +# of some type for the subnet in question. +# In this case the netmask is implied (it comes from the network +# configuration on the machine running dnsmasq) it is possible to give +# an explicit netmask instead. +#dhcp-range=192.168.0.0,static + +# Enable DHCPv6. Note that the prefix-length does not need to be specified +# and defaults to 64 if missing/ +#dhcp-range=1234::2, 1234::500, 64, 12h + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +#dhcp-range=1234::, ra-only + +# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and +# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack +# hosts. Use the DHCPv4 lease to derive the name, network segment and +# MAC address and assume that the host will also have an +# IPv6 address calculated using the SLAAC alogrithm. +#dhcp-range=1234::, ra-names + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.) +#dhcp-range=1234::, ra-only, 48h + +# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA +# so that clients can use SLAAC addresses as well as DHCP ones. +#dhcp-range=1234::2, 1234::500, slaac + +# Do Router Advertisements and stateless DHCP for this subnet. Clients will +# not get addresses from DHCP, but they will get other configuration information. +# They will use SLAAC for addresses. +#dhcp-range=1234::, ra-stateless + +# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses +# from DHCPv4 leases. +#dhcp-range=1234::, ra-stateless, ra-names + +# Do router advertisements for all subnets where we're doing DHCPv6 +# Unless overriden by ra-stateless, ra-names, et al, the router +# advertisements will have the M and O bits set, so that the clients +# get addresses and configuration from DHCPv6, and the A bit reset, so the +# clients don't use SLAAC addresses. +#enable-ra + +# Supply parameters for specified hosts using DHCP. There are lots +# of valid alternatives, so we will give examples of each. Note that +# IP addresses DO NOT have to be in the range given above, they just +# need to be on the same network. The order of the parameters in these +# do not matter, it's permissible to give name, address and MAC in any +# order. + +# Always allocate the host with Ethernet address 11:22:33:44:55:66 +# The IP address 192.168.0.60 +#dhcp-host=11:22:33:44:55:66,192.168.0.60 + +# Always set the name of the host with hardware address +# 11:22:33:44:55:66 to be "fred" +#dhcp-host=11:22:33:44:55:66,fred + +# Always give the host with Ethernet address 11:22:33:44:55:66 +# the name fred and IP address 192.168.0.60 and lease time 45 minutes +#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m + +# Give a host with Ethernet address 11:22:33:44:55:66 or +# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume +# that these two Ethernet interfaces will never be in use at the same +# time, and give the IP address to the second, even if it is already +# in use by the first. Useful for laptops with wired and wireless +# addresses. +#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60 + +# Give the machine which says its name is "bert" IP address +# 192.168.0.70 and an infinite lease +#dhcp-host=bert,192.168.0.70,infinite + +# Always give the host with client identifier 01:02:02:04 +# the IP address 192.168.0.60 +#dhcp-host=id:01:02:02:04,192.168.0.60 + +# Always give the Infiniband interface with hardware address +# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the +# ip address 192.168.0.61. The client id is derived from the prefix +# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of +# hex digits of the hardware address. +#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61 + +# Always give the host with client identifier "marjorie" +# the IP address 192.168.0.60 +#dhcp-host=id:marjorie,192.168.0.60 + +# Enable the address given for "judge" in /etc/hosts +# to be given to a machine presenting the name "judge" when +# it asks for a DHCP lease. +#dhcp-host=judge + +# Never offer DHCP service to a machine whose Ethernet +# address is 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,ignore + +# Ignore any client-id presented by the machine with Ethernet +# address 11:22:33:44:55:66. This is useful to prevent a machine +# being treated differently when running under different OS's or +# between PXE boot and OS boot. +#dhcp-host=11:22:33:44:55:66,id:* + +# Send extra options which are tagged as "red" to +# the machine with Ethernet address 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,set:red + +# Send extra options which are tagged as "red" to +# any machine with Ethernet address starting 11:22:33: +#dhcp-host=11:22:33:*:*:*,set:red + +# Give a fixed IPv6 address and name to client with +# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 +# Note the MAC addresses CANNOT be used to identify DHCPv6 clients. +# Note also the they [] around the IPv6 address are obilgatory. +#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] + +# Ignore any clients which are not specified in dhcp-host lines +# or /etc/ethers. Equivalent to ISC "deny unknown-clients". +# This relies on the special "known" tag which is set when +# a host is matched. +#dhcp-ignore=tag:!known + +# Send extra options which are tagged as "red" to any machine whose +# DHCP vendorclass string includes the substring "Linux" +#dhcp-vendorclass=set:red,Linux + +# Send extra options which are tagged as "red" to any machine one +# of whose DHCP userclass strings includes the substring "accounts" +#dhcp-userclass=set:red,accounts + +# Send extra options which are tagged as "red" to any machine whose +# MAC address matches the pattern. +#dhcp-mac=set:red,00:60:8C:*:*:* + +# If this line is uncommented, dnsmasq will read /etc/ethers and act +# on the ethernet-address/IP pairs found there just as if they had +# been given as --dhcp-host options. Useful if you keep +# MAC-address/host mappings there for other purposes. +#read-ethers + +# Send options to hosts which ask for a DHCP lease. +# See RFC 2132 for details of available options. +# Common options can be given to dnsmasq by name: +# run "dnsmasq --help dhcp" to get a list. +# Note that all the common settings, such as netmask and +# broadcast address, DNS server and default route, are given +# sane defaults by dnsmasq. You very likely will not need +# any dhcp-options. If you use Windows clients and Samba, there +# are some options which are recommended, they are detailed at the +# end of this section. + +# Override the default route supplied by dnsmasq, which assumes the +# router is the same machine as the one running dnsmasq. +#dhcp-option=3,1.2.3.4 + +# Do the same thing, but using the option name +#dhcp-option=option:router,1.2.3.4 + +# Override the default route supplied by dnsmasq and send no default +# route at all. Note that this only works for the options sent by +# default (1, 3, 6, 12, 28) the same line will send a zero-length option +# for all other option numbers. +#dhcp-option=3 + +# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5 +#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5 + +# Send DHCPv6 option. Note [] around IPv6 addresses. +#dhcp-option=option6:dns-server,[1234::77],[1234::88] + +# Send DHCPv6 option for namservers as the machine running +# dnsmasq and another. +#dhcp-option=option6:dns-server,[::],[1234::88] + +# Ask client to poll for option changes every six hours. (RFC4242) +#dhcp-option=option6:information-refresh-time,6h + +# Set option 58 client renewal time (T1). Defaults to half of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T1:1m + +# Set option 59 rebinding time (T2). Defaults to 7/8 of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T2:2m + +# Set the NTP time server address to be the same machine as +# is running dnsmasq +#dhcp-option=42,0.0.0.0 + +# Set the NIS domain name to "welly" +#dhcp-option=40,welly + +# Set the default time-to-live to 50 +#dhcp-option=23,50 + +# Set the "all subnets are local" flag +#dhcp-option=27,1 + +# Send the etherboot magic flag and then etherboot options (a string). +#dhcp-option=128,e4:45:74:68:00:00 +#dhcp-option=129,NIC=eepro100 + +# Specify an option which will only be sent to the "red" network +# (see dhcp-range for the declaration of the "red" network) +# Note that the tag: part must precede the option: part. +#dhcp-option = tag:red, option:ntp-server, 192.168.1.1 + +# The following DHCP options set up dnsmasq in the same way as is specified +# for the ISC dhcpcd in +# http://www.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt +# adapted for a typical dnsmasq installation where the host running +# dnsmasq is also the host running samba. +# you may want to uncomment some or all of them if you use +# Windows clients and Samba. +#dhcp-option=19,0 # option ip-forwarding off +#dhcp-option=44,0.0.0.0 # set netbios-over-TCP/IP nameserver(s) aka WINS server(s) +#dhcp-option=45,0.0.0.0 # netbios datagram distribution server +#dhcp-option=46,8 # netbios node type + +# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave. +#dhcp-option=252,"\n" + +# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client +# probably doesn't support this...... +#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com + +# Send RFC-3442 classless static routes (note the netmask encoding) +#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8 + +# Send vendor-class specific options encapsulated in DHCP option 43. +# The meaning of the options is defined by the vendor-class so +# options are sent only when the client supplied vendor class +# matches the class given here. (A substring match is OK, so "MSFT" +# matches "MSFT" and "MSFT 5.0"). This example sets the +# mtftp address to 0.0.0.0 for PXEClients. +#dhcp-option=vendor:PXEClient,1,0.0.0.0 + +# Send microsoft-specific option to tell windows to release the DHCP lease +# when it shuts down. Note the "i" flag, to tell dnsmasq to send the +# value as a four-byte integer - that's what microsoft wants. See +# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true +#dhcp-option=vendor:MSFT,2,1i + +# Send the Encapsulated-vendor-class ID needed by some configurations of +# Etherboot to allow is to recognise the DHCP server. +#dhcp-option=vendor:Etherboot,60,"Etherboot" + +# Send options to PXELinux. Note that we need to send the options even +# though they don't appear in the parameter request list, so we need +# to use dhcp-option-force here. +# See http://syslinux.zytor.com/pxe.php#special for details. +# Magic number - needed before anything else is recognised +#dhcp-option-force=208,f1:00:74:7e +# Configuration file name +#dhcp-option-force=209,configs/common +# Path prefix +#dhcp-option-force=210,/tftpboot/pxelinux/files/ +# Reboot time. (Note 'i' to send 32-bit value) +#dhcp-option-force=211,30i + +# Set the boot filename for netboot/PXE. You will only need +# this is you want to boot machines over the network and you will need +# a TFTP server; either dnsmasq's built in TFTP server or an +# external one. (See below for how to enable the TFTP server.) +#dhcp-boot=pxelinux.0 + +# The same as above, but use custom tftp-server instead machine running dnsmasq +#dhcp-boot=pxelinux,server.name,192.168.1.100 + +# Boot for Etherboot gPXE. The idea is to send two different +# filenames, the first loads gPXE, and the second tells gPXE what to +# load. The dhcp-match sets the gpxe tag for requests from gPXE. +#dhcp-match=set:gpxe,175 # gPXE sends a 175 option. +#dhcp-boot=tag:!gpxe,undionly.kpxe +#dhcp-boot=mybootimage + +# Encapsulated options for Etherboot gPXE. All the options are +# encapsulated within option 175 +#dhcp-option=encap:175, 1, 5b # priority code +#dhcp-option=encap:175, 176, 1b # no-proxydhcp +#dhcp-option=encap:175, 177, string # bus-id +#dhcp-option=encap:175, 189, 1b # BIOS drive code +#dhcp-option=encap:175, 190, user # iSCSI username +#dhcp-option=encap:175, 191, pass # iSCSI password + +# Test for the architecture of a netboot client. PXE clients are +# supposed to send their architecture as option 93. (See RFC 4578) +#dhcp-match=peecees, option:client-arch, 0 #x86-32 +#dhcp-match=itanics, option:client-arch, 2 #IA64 +#dhcp-match=hammers, option:client-arch, 6 #x86-64 +#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64 + +# Do real PXE, rather than just booting a single file, this is an +# alternative to dhcp-boot. +#pxe-prompt="What system shall I netboot?" +# or with timeout before first available action is taken: +#pxe-prompt="Press F8 for menu.", 60 + +# Available boot services. for PXE. +#pxe-service=x86PC, "Boot from local disk" + +# Loads /pxelinux.0 from dnsmasq TFTP server. +#pxe-service=x86PC, "Install Linux", pxelinux + +# Loads /pxelinux.0 from TFTP server at 1.2.3.4. +# Beware this fails on old PXE ROMS. +#pxe-service=x86PC, "Install Linux", pxelinux, 1.2.3.4 + +# Use bootserver on network, found my multicast or broadcast. +#pxe-service=x86PC, "Install windows from RIS server", 1 + +# Use bootserver at a known IP address. +#pxe-service=x86PC, "Install windows from RIS server", 1, 1.2.3.4 + +# If you have multicast-FTP available, +# information for that can be passed in a similar way using options 1 +# to 5. See page 19 of +# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf + + +# Enable dnsmasq's built-in TFTP server +#enable-tftp + +# Set the root directory for files available via FTP. +#tftp-root=/var/ftpd + +# Do not abort if the tftp-root is unavailable +#tftp-no-fail + +# Make the TFTP server more secure: with this set, only files owned by +# the user dnsmasq is running as will be send over the net. +#tftp-secure + +# This option stops dnsmasq from negotiating a larger blocksize for TFTP +# transfers. It will slow things down, but may rescue some broken TFTP +# clients. +#tftp-no-blocksize + +# Set the boot file name only when the "red" tag is set. +#dhcp-boot=tag:red,pxelinux.red-net + +# An example of dhcp-boot with an external TFTP server: the name and IP +# address of the server are given after the filename. +# Can fail with old PXE ROMS. Overridden by --pxe-service. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3 + +# If there are multiple external tftp servers having a same name +# (using /etc/hosts) then that name can be specified as the +# tftp_servername (the third option to dhcp-boot) and in that +# case dnsmasq resolves this name and returns the resultant IP +# addresses in round robin fasion. This facility can be used to +# load balance the tftp load among a set of servers. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name + +# Set the limit on DHCP leases, the default is 150 +#dhcp-lease-max=150 + +# The DHCP server needs somewhere on disk to keep its lease database. +# This defaults to a sane location, but if you want to change it, use +# the line below. +#dhcp-leasefile=/var/lib/misc/dnsmasq.leases + +# Set the DHCP server to authoritative mode. In this mode it will barge in +# and take over the lease for any client which broadcasts on the network, +# whether it has a record of the lease or not. This avoids long timeouts +# when a machine wakes up on a new network. DO NOT enable this if there's +# the slightest chance that you might end up accidentally configuring a DHCP +# server for your campus/company accidentally. The ISC server uses +# the same option, and this URL provides more information: +# http://www.isc.org/files/auth.html +#dhcp-authoritative + +# Run an executable when a DHCP lease is created or destroyed. +# The arguments sent to the script are "add" or "del", +# then the MAC address, the IP address and finally the hostname +# if there is one. +#dhcp-script=/bin/echo + +# Set the cachesize here. +#cache-size=150 + +# If you want to disable negative caching, uncomment this. +#no-negcache + +# Normally responses which come from /etc/hosts and the DHCP lease +# file have Time-To-Live set as zero, which conventionally means +# do not cache further. If you are happy to trade lower load on the +# server for potentially stale date, you can set a time-to-live (in +# seconds) here. +#local-ttl= + +# If you want dnsmasq to detect attempts by Verisign to send queries +# to unregistered .com and .net hosts to its sitefinder service and +# have dnsmasq instead return the correct NXDOMAIN response, uncomment +# this line. You can add similar lines to do the same for other +# registries which have implemented wildcard A records. +#bogus-nxdomain=64.94.110.11 + +# If you want to fix up DNS results from upstream servers, use the +# alias option. This only works for IPv4. +# This alias makes a result of 1.2.3.4 appear as 5.6.7.8 +#alias=1.2.3.4,5.6.7.8 +# and this maps 1.2.3.x to 5.6.7.x +#alias=1.2.3.0,5.6.7.0,255.255.255.0 +# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40 +#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0 + +# Change these lines if you want dnsmasq to serve MX records. + +# Return an MX record named "maildomain.com" with target +# servermachine.com and preference 50 +#mx-host=maildomain.com,servermachine.com,50 + +# Set the default target for MX records created using the localmx option. +#mx-target=servermachine.com + +# Return an MX record pointing to the mx-target for all local +# machines. +#localmx + +# Return an MX record pointing to itself for all local machines. +#selfmx + +# Change the following lines if you want dnsmasq to serve SRV +# records. These are useful if you want to serve ldap requests for +# Active Directory and other windows-originated DNS requests. +# See RFC 2782. +# You may add multiple srv-host lines. +# The fields are ,,,, +# If the domain part if missing from the name (so that is just has the +# service and protocol sections) then the domain given by the domain= +# config option is used. (Note that expand-hosts does not need to be +# set for this to work.) + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389 + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 (using domain=) +#domain=example.com +#srv-host=_ldap._tcp,ldapserver.example.com,389 + +# Two SRV records for LDAP, each with different priorities +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2 + +# A SRV record indicating that there is no LDAP server for the domain +# example.com +#srv-host=_ldap._tcp.example.com + +# The following line shows how to make dnsmasq serve an arbitrary PTR +# record. This is useful for DNS-SD. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for PTR records.) +#ptr-record=_http._tcp.dns-sd-services,"New Employee Page._http._tcp.dns-sd-services" + +# Change the following lines to enable dnsmasq to serve TXT records. +# These are used for things like SPF and zeroconf. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for TXT records.) + +#Example SPF. +#txt-record=example.com,"v=spf1 a -all" + +#Example zeroconf +#txt-record=_http._tcp.example.com,name=value,paper=A4 + +# Provide an alias for a "local" DNS name. Note that this _only_ works +# for targets which are names from DHCP or /etc/hosts. Give host +# "bert" another name, bertrand +#cname=bertand,bert + +# For debugging purposes, log each DNS query as it passes through +# dnsmasq. +#log-queries + +# Log lots of extra information about DHCP transactions. +#log-dhcp + +# Include another lot of configuration options. +#conf-file=/etc/dnsmasq.more.conf +#conf-dir=/etc/dnsmasq.d + +# Include all the files in a directory except those ending in .bak +#conf-dir=/etc/dnsmasq.d,.bak + +# Include all files in a directory which end in .conf +#conf-dir=/etc/dnsmasq.d/,*.conf +# diff --git a/roles/features/templates/pagespeed.conf.j2 b/roles/features/templates/pagespeed.conf.j2 new file mode 100644 index 00000000..3b89b758 --- /dev/null +++ b/roles/features/templates/pagespeed.conf.j2 @@ -0,0 +1,369 @@ + + # Turn on mod_pagespeed. To completely disable mod_pagespeed, you + # can set this to "off". + ModPagespeed on + + # We want VHosts to inherit global configuration. + # If this is not included, they'll be independent (except for inherently + # global options), at least for backwards compatibility. + ModPagespeedInheritVHostConfig on + + # Direct Apache to send all HTML output to the mod_pagespeed + # output handler. + AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html + + # If you want mod_pagespeed process XHTML as well, please uncomment this + # line. + # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml + + # The ModPagespeedFileCachePath directory must exist and be writable + # by the apache user (as specified by the User directive). + ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" + + # LogDir is needed to store various logs, including the statistics log + # required for the console. + ModPagespeedLogDir "/var/log/pagespeed" + + # The locations of SSL Certificates is distribution-dependent. + ModPagespeedSslCertDirectory "/etc/ssl/certs" + + + # If you want, you can use one or more memcached servers as the store for + # the mod_pagespeed cache. + # ModPagespeedMemcachedServers localhost:11211 + + # A portion of the cache can be kept in memory only, to reduce load on disk + # (or memcached) from many small files. + # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 + + # Override the mod_pagespeed 'rewrite level'. The default level + # "CoreFilters" uses a set of rewrite filters that are generally + # safe for most web pages. Most sites should not need to change + # this value and can instead fine-tune the configuration using the + # ModPagespeedDisableFilters and ModPagespeedEnableFilters + # directives, below. Valid values for ModPagespeedRewriteLevel are + # PassThrough, CoreFilters and TestingCoreFilters. + # + ModPagespeedRewriteLevel CoreFilters + + ModPagespeedEnableFilters combine_heads + ModPagespeedEnableFilters combine_javascript + ModPagespeedEnableFilters convert_jpeg_to_webp + ModPagespeedEnableFilters convert_png_to_jpeg + ModPagespeedEnableFilters inline_preview_images + ModPagespeedEnableFilters make_google_analytics_async + ModPagespeedEnableFilters move_css_above_scripts + ModPagespeedEnableFilters move_css_to_head + ModPagespeedEnableFilters resize_mobile_images + ModPagespeedEnableFilters sprite_images + + ModPagespeedEnableFilters defer_iframe + ModPagespeedEnableFilters defer_javascript + ModPagespeedEnableFilters lazyload_images + + # Explicitly disables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, if one + # of the filters in the CoreFilters needs to be disabled for a + # site, that filter can be added to + # ModPagespeedDisableFilters. This directive contains a + # comma-separated list of filter names, and can be repeated. + # + # ModPagespeedDisableFilters rewrite_images + + # Explicitly enables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, filters + # not included in the CoreFilters may be enabled using this + # directive. This directive contains a comma-separated list of + # filter names, and can be repeated. + # + # ModPagespeedEnableFilters rewrite_javascript,rewrite_css + # ModPagespeedEnableFilters collapse_whitespace,elide_attributes + + # Explicitly forbids the enabling of specific filters using either query + # parameters or request headers. This is useful, for example, when we do + # not want the filter to run for performance or security reasons. This + # directive contains a comma-separated list of filter names, and can be + # repeated. + # + # ModPagespeedForbidFilters rewrite_images + + # How long mod_pagespeed will wait to return an optimized resource + # (per flush window) on first request before giving up and returning the + # original (unoptimized) resource. After this deadline is exceeded the + # original resource is returned and the optimization is pushed to the + # background to be completed for future requests. Increasing this value will + # increase page latency, but might reduce load time (for instance on a + # bandwidth-constrained link where it's worth waiting for image + # compression to complete). If the value is less than or equal to zero + # mod_pagespeed will wait indefinitely for the rewrite to complete before + # returning. + # + # ModPagespeedRewriteDeadlinePerFlushMs 10 + + # ModPagespeedDomain + # authorizes rewriting of JS, CSS, and Image files found in this + # domain. By default only resources with the same origin as the + # HTML file are rewritten. For example: + # + ModPagespeedDomain * + # + # This will allow resources found on http://cdn.myhost.com to be + # rewritten in addition to those in the same domain as the HTML. + # + # Other domain-related directives (like ModPagespeedMapRewriteDomain + # and ModPagespeedMapOriginDomain) can also authorize domains. + # + # Wildcards (* and ?) are allowed in the domain specification. Be + # careful when using them as if you rewrite domains that do not + # send you traffic, then the site receiving the traffic will not + # know how to serve the rewritten content. + + # If you use downstream caches such as varnish or proxy_cache for caching + # HTML, you can configure pagespeed to work with these caches correctly + # using the following directives. Note that the values for + # ModPagespeedDownstreamCachePurgeLocationPrefix and + # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here + # in order to force the webmaster to choose appropriate value for these. + # + # ModPagespeedDownstreamCachePurgeLocationPrefix + # ModPagespeedDownstreamCachePurgeMethod PURGE + # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 + # ModPagespeedDownstreamCacheRebeaconingKey + + # Other defaults (cache sizes and thresholds): + # + # ModPagespeedFileCacheSizeKb 102400 + # ModPagespeedFileCacheCleanIntervalMs 3600000 + # ModPagespeedLRUCacheKbPerProcess 1024 + # ModPagespeedLRUCacheByteLimit 16384 + # ModPagespeedCssFlattenMaxBytes 102400 + # ModPagespeedCssInlineMaxBytes 2048 + # ModPagespeedCssImageInlineMaxBytes 0 + # ModPagespeedImageInlineMaxBytes 3072 + # ModPagespeedJsInlineMaxBytes 2048 + # ModPagespeedCssOutlineMinBytes 3000 + # ModPagespeedJsOutlineMinBytes 3000 + # ModPagespeedMaxCombinedCssBytes -1 + # ModPagespeedMaxCombinedJsBytes 92160 + + # Limit the number of inodes in the file cache. Set to 0 for no limit. + # The default value if this paramater is not specified is 0 (no limit). + ModPagespeedFileCacheInodeLimit 500000 + + # Bound the number of images that can be rewritten at any one time; this + # avoids overloading the CPU. Set this to 0 to remove the bound. + # + # ModPagespeedImageMaxRewritesAtOnce 8 + + # You can also customize the number of threads per Apache process + # mod_pagespeed will use to do resource optimization. Plain + # "rewrite threads" are used to do short, latency-sensitive work, + # while "expensive rewrite threads" are used for actual optimization + # work that's more computationally expensive. If you live these unset, + # or use values <= 0 the defaults will be used, which is 1 for both + # values when using non-threaded MPMs (e.g. prefork) and 4 for both + # on threaded MPMs (e.g. worker and event). These settings can only + # be changed globally, and not per virtual host. + # + # ModPagespeedNumRewriteThreads 4 + # ModPagespeedNumExpensiveRewriteThreads 4 + + # Randomly drop rewrites (*) to increase the chance of optimizing + # frequently fetched resources and decrease the chance of optimizing + # infrequently fetched resources. This can reduce CPU load. The default + # value of this parameter is 0 (no drops). 90 means that a resourced + # fetched once has a 10% probability of being optimized while a resource + # that is fetched 50 times has a 99.65% probability of being optimized. + # + # (*) Currently only CSS files and images are randomly dropped. Images + # within CSS files are not randomly dropped. + # + # ModPagespeedRewriteRandomDropPercentage 90 + + # Many filters modify the URLs of resources in HTML files. This is typically + # harmless but pages whose Javascript expects to read or modify the original + # URLs may break. The following parameters prevent filters from modifying + # URLs of their respective types. + # + # ModPagespeedJsPreserveURLs on + # ModPagespeedImagePreserveURLs on + # ModPagespeedCssPreserveURLs on + + # When PreserveURLs is on, it is still possible to enable browser-specific + # optimizations (for example, webp images can be served to browsers that + # will accept them). They'll be served with Vary: Accept or Vary: + # User-Agent headers as appropriate. Note that this may require configuring + # reverse proxy caches such as varnish to handle these headers properly. + # + # ModPagespeedFilters in_place_optimize_for_browser + + # Internet Explorer has difficulty caching resources with Vary: headers. + # They will either be uncached (older IE) or require revalidation. See: + # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx + # As a result we serve them as Cache-Control: private instead by default. + # If you are using a reverse proxy or CDN configured to cache content with + # the Vary: Accept header you should turn this setting off. + # + # ModPagespeedPrivateNotVaryForIE on + + # Settings for image optimization: + # + # Lossy image recompression quality (0 to 100, -1 just strips metadata): + # ModPagespeedImageRecompressionQuality 85 + # + # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedJpegRecompressionQuality -1 + # ModPagespeedJpegRecompressionQualityForSmallScreens 70 + + ModPagespeedJpegRecompressionQuality 75 + + # + # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedWebpRecompressionQuality 80 + # ModPagespeedWebpRecompressionQualityForSmallScreens 70 + # + # Timeout for conversions to WebP format, in + # milliseconds. Negative values mean no timeout is applied. The + # default value is -1: + # ModPagespeedWebpTimeoutMs 5000 + # + # Percent of original image size below which optimized images are retained: + # ModPagespeedImageLimitOptimizedPercent 100 + # + # Percent of original image area below which image resizing will be + # attempted: + # ModPagespeedImageLimitResizeAreaPercent 100 + + # Settings for inline preview images + # + # Setting this to n restricts preview images to the first n images found on + # the page. The default of -1 means preview images can appear anywhere on + # the page (if those images appear above the fold). + # ModPagespeedMaxInlinedPreviewImagesIndex -1 + + # Sets the minimum size in bytes of any image for which a low quality image + # is generated. + # ModPagespeedMinImageSizeLowResolutionBytes 3072 + + # The maximum URL size is generally limited to about 2k characters + # due to IE: See http://support.microsoft.com/kb/208427/EN-US. + # Apache servers by default impose a further limitation of about + # 250 characters per URL segment (text between slashes). + # mod_pagespeed circumvents this limitation, but if you employ + # proxy servers in your path you may need to re-impose it by + # overriding the setting here. The default setting is 1024 + # characters. + # + # ModPagespeedMaxSegmentLength 250 + + # Uncomment this if you want to prevent mod_pagespeed from combining files + # (e.g. CSS files) across paths + # + # ModPagespeedCombineAcrossPaths off + + # Renaming JavaScript URLs can sometimes break them. With this + # option enabled, mod_pagespeed uses a simple heuristic to decide + # not to rename JavaScript that it thinks is introspective. + # + # You can uncomment this to let mod_pagespeed rename all JS files. + # + # ModPagespeedAvoidRenamingIntrospectiveJavascript off + + # Certain common JavaScript libraries are available from Google, which acts + # as a CDN and allows you to benefit from browser caching if a new visitor + # to your site previously visited another site that makes use of the same + # libraries as you do. Enable the following filter to turn on this feature. + # + # ModPagespeedEnableFilters canonicalize_javascript_libraries + + # The following line configures a library that is recognized by + # canonicalize_javascript_libraries. This will have no effect unless you + # enable this filter (generally by uncommenting the last line in the + # previous stanza). The format is: + # ModPagespeedLibrary bytes md5 canonical_url + # Where bytes and md5 are with respect to the *minified* JS; use + # js_minify --print_size_and_hash to obtain this data. + # Note that we can register multiple hashes for the same canonical url; + # we do this if there are versions available that have already been minified + # with more sophisticated tools. + # + # Additional library configuration can be found in + # pagespeed_libraries.conf included in the distribution. You should add + # new entries here, though, so that file can be automatically upgraded. + # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js + + # Explicitly tell mod_pagespeed to load some resources from disk. + # This will speed up load time and update frequency. + # + # This should only be used for static resources which do not need + # specific headers set or other processing by Apache. + # + # Both URL and filesystem path should specify directories and + # filesystem path must be absolute (for now). + # + # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" + + + # Enables server-side instrumentation and statistics. If this rewriter is + # enabled, then each rewritten HTML page will have instrumentation javacript + # added that sends latency beacons to /mod_pagespeed_beacon. These + # statistics can be accessed at /mod_pagespeed_statistics. You must also + # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers + # below. + # + # ModPagespeedEnableFilters add_instrumentation + + # The add_instrumentation filter sends a beacon after the page onload + # handler is called. The user might navigate to a new URL before this. If + # you enable the following directive, the beacon is sent as part of an + # onbeforeunload handler, for pages where navigation happens before the + # onload event. + # + # ModPagespeedReportUnloadTime on + + # Uncomment the following line so that ModPagespeed will not cache or + # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. + # Note that ModPagespeed always respects Vary: headers on html content. + # ModPagespeedRespectVary on + + # Uncomment the following line if you want to disable statistics entirely. + # + # ModPagespeedStatistics off + + # These handlers are central entry-points into the admin pages. + # By default, pagespeed_admin and pagespeed_global_admin present + # the same data, and differ only when + # ModPagespeedUsePerVHostStatistics is enabled. In that case, + # /pagespeed_global_admin sees aggregated data across all vhosts, + # and the /pagespeed_admin sees data only for a particular vhost. + # + # You may insert other "Allow from" lines to add hosts you want to + # allow to look at generated statistics. Another possibility is + # to comment out the "Order" and "Allow" options from the config + # file, to allow any client that can reach your server to access + # and change server state, such as statistics, caches, and + # messages. This might be appropriate in an experimental setup. + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_admin + + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_global_admin + + + # Enable logging of mod_pagespeed statistics, needed for the console. + ModPagespeedStatisticsLogging on + + # Page /mod_pagespeed_message lets you view the latest messages from + # mod_pagespeed, regardless of log-level in your httpd.conf + # ModPagespeedMessageBufferSize is the maximum number of bytes you would + # like to dump to your /mod_pagespeed_message page at one time, + # its default value is 100k bytes. + # Set it to 0 if you want to disable this feature. + ModPagespeedMessageBufferSize 100000 + diff --git a/roles/features/templates/ports.conf.j2 b/roles/features/templates/ports.conf.j2 new file mode 100644 index 00000000..2618436c --- /dev/null +++ b/roles/features/templates/ports.conf.j2 @@ -0,0 +1,13 @@ +# If you just change the port or add more ports here, you will likely also +# have to change the VirtualHost statement in +# /etc/apache2/sites-enabled/000-default.conf + +Listen 172.16.0.1:8080 + + + Listen 172.16.0.1:443 + + + + Listen 172.16.0.1:443 + diff --git a/roles/features/templates/privoxy_config.j2 b/roles/features/templates/privoxy_config.j2 new file mode 100644 index 00000000..dd55f0f3 --- /dev/null +++ b/roles/features/templates/privoxy_config.j2 @@ -0,0 +1,2107 @@ +# Sample Configuration File for Privoxy +# +# Id: config,v +# +# Copyright (C) 2001-2014 Privoxy Developers http://www.privoxy.org/ +# +#################################################################### +# # +# Table of Contents # +# # +# I. INTRODUCTION # +# II. FORMAT OF THE CONFIGURATION FILE # +# # +# 1. LOCAL SET-UP DOCUMENTATION # +# 2. CONFIGURATION AND LOG FILE LOCATIONS # +# 3. DEBUGGING # +# 4. ACCESS CONTROL AND SECURITY # +# 5. FORWARDING # +# 6. MISCELLANEOUS # +# 7. WINDOWS GUI OPTIONS # +# # +#################################################################### +# +# +# I. INTRODUCTION +# =============== +# +# This file holds Privoxy's main configuration. Privoxy detects +# configuration changes automatically, so you don't have to restart +# it unless you want to load a different configuration file. +# +# The configuration will be reloaded with the first request after +# the change was done, this request itself will still use the old +# configuration, though. In other words: it takes two requests +# before you see the result of your changes. Requests that are +# dropped due to ACL don't trigger reloads. +# +# When starting Privoxy on Unix systems, give the location of this +# file as last argument. On Windows systems, Privoxy will look for +# this file with the name 'config.txt' in the current working +# directory of the Privoxy process. +# +# +# II. FORMAT OF THE CONFIGURATION FILE +# ==================================== +# +# Configuration lines consist of an initial keyword followed by a +# list of values, all separated by whitespace (any number of spaces +# or tabs). For example, +# +# actionsfile default.action +# +# Indicates that the actionsfile is named 'default.action'. +# +# The '#' indicates a comment. Any part of a line following a '#' is +# ignored, except if the '#' is preceded by a '\'. +# +# Thus, by placing a # at the start of an existing configuration +# line, you can make it a comment and it will be treated as if it +# weren't there. This is called "commenting out" an option and can +# be useful. Removing the # again is called "uncommenting". +# +# Note that commenting out an option and leaving it at its default +# are two completely different things! Most options behave very +# differently when unset. See the "Effect if unset" explanation in +# each option's description for details. +# +# Long lines can be continued on the next line by using a `\' as the +# last character. +# +# +# 1. LOCAL SET-UP DOCUMENTATION +# ============================== +# +# If you intend to operate Privoxy for more users than just +# yourself, it might be a good idea to let them know how to reach +# you, what you block and why you do that, your policies, etc. +# +# +# 1.1. user-manual +# ================= +# +# Specifies: +# +# Location of the Privoxy User Manual. +# +# Type of value: +# +# A fully qualified URI +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# http://www.privoxy.org/version/user-manual/ will be used, +# where version is the Privoxy version. +# +# Notes: +# +# The User Manual URI is the single best source of information +# on Privoxy, and is used for help links from some of the +# internal CGI pages. The manual itself is normally packaged +# with the binary distributions, so you probably want to set +# this to a locally installed copy. +# +# Examples: +# +# The best all purpose solution is simply to put the full local +# PATH to where the User Manual is located: +# +# user-manual /usr/share/doc/privoxy/user-manual +# +# The User Manual is then available to anyone with access to +# Privoxy, by following the built-in URL: http:// +# config.privoxy.org/user-manual/ (or the shortcut: http://p.p/ +# user-manual/). +# +# If the documentation is not on the local system, it can be +# accessed from a remote server, as: +# +# user-manual http://example.com/privoxy/user-manual/ +# +# WARNING!!! +# +# If set, this option should be the first option in the +# config file, because it is used while the config file is +# being read. +# +user-manual /usr/share/doc/privoxy/user-manual +# +# 1.2. trust-info-url +# ==================== +# +# Specifies: +# +# A URL to be displayed in the error page that users will see if +# access to an untrusted page is denied. +# +# Type of value: +# +# URL +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No links are displayed on the "untrusted" error page. +# +# Notes: +# +# The value of this option only matters if the experimental +# trust mechanism has been activated. (See trustfile below.) +# +# If you use the trust mechanism, it is a good idea to write up +# some on-line documentation about your trust policy and to +# specify the URL(s) here. Use multiple times for multiple URLs. +# +# The URL(s) should be added to the trustfile as well, so users +# don't end up locked out from the information on why they were +# locked out in the first place! +# +#trust-info-url http://www.example.com/why_we_block.html +#trust-info-url http://www.example.com/what_we_allow.html +# +# 1.3. admin-address +# =================== +# +# Specifies: +# +# An email address to reach the Privoxy administrator. +# +# Type of value: +# +# Email address +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No email address is displayed on error pages and the CGI user +# interface. +# +# Notes: +# +# If both admin-address and proxy-info-url are unset, the whole +# "Local Privoxy Support" box on all generated pages will not be +# shown. +# +#admin-address privoxy-admin@example.com +# +# 1.4. proxy-info-url +# ==================== +# +# Specifies: +# +# A URL to documentation about the local Privoxy setup, +# configuration or policies. +# +# Type of value: +# +# URL +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No link to local documentation is displayed on error pages and +# the CGI user interface. +# +# Notes: +# +# If both admin-address and proxy-info-url are unset, the whole +# "Local Privoxy Support" box on all generated pages will not be +# shown. +# +# This URL shouldn't be blocked ;-) +# +#proxy-info-url http://www.example.com/proxy-service.html +# +# 2. CONFIGURATION AND LOG FILE LOCATIONS +# ======================================== +# +# Privoxy can (and normally does) use a number of other files for +# additional configuration, help and logging. This section of the +# configuration file tells Privoxy where to find those other files. +# +# The user running Privoxy, must have read permission for all +# configuration files, and write permission to any files that would +# be modified, such as log files and actions files. +# +# +# 2.1. confdir +# ============= +# +# Specifies: +# +# The directory where the other configuration files are located. +# +# Type of value: +# +# Path name +# +# Default value: +# +# /etc/privoxy (Unix) or Privoxy installation dir (Windows) +# +# Effect if unset: +# +# Mandatory +# +# Notes: +# +# No trailing "/", please. +# +confdir /etc/privoxy +# +# 2.2. templdir +# ============== +# +# Specifies: +# +# An alternative directory where the templates are loaded from. +# +# Type of value: +# +# Path name +# +# Default value: +# +# unset +# +# Effect if unset: +# +# The templates are assumed to be located in confdir/template. +# +# Notes: +# +# Privoxy's original templates are usually overwritten with each +# update. Use this option to relocate customized templates that +# should be kept. As template variables might change between +# updates, you shouldn't expect templates to work with Privoxy +# releases other than the one they were part of, though. +# +#templdir . +# +# 2.3. temporary-directory +# ========================= +# +# Specifies: +# +# A directory where Privoxy can create temporary files. +# +# Type of value: +# +# Path name +# +# Default value: +# +# unset +# +# Effect if unset: +# +# No temporary files are created, external filters don't work. +# +# Notes: +# +# To execute external filters, Privoxy has to create temporary +# files. This directive specifies the directory the temporary +# files should be written to. +# +# It should be a directory only Privoxy (and trusted users) can +# access. +# +#temporary-directory . +# +# 2.4. logdir +# ============ +# +# Specifies: +# +# The directory where all logging takes place (i.e. where the +# logfile is located). +# +# Type of value: +# +# Path name +# +# Default value: +# +# /var/log/privoxy (Unix) or Privoxy installation dir (Windows) +# +# Effect if unset: +# +# Mandatory +# +# Notes: +# +# No trailing "/", please. +# +logdir /var/log/privoxy +# +# 2.5. actionsfile +# ================= +# +# Specifies: +# +# The actions file(s) to use +# +# Type of value: +# +# Complete file name, relative to confdir +# +# Default values: +# +# match-all.action # Actions that are applied to all sites and maybe overruled later on. +# +# default.action # Main actions file +# +# user.action # User customizations +# +# Effect if unset: +# +# No actions are taken at all. More or less neutral proxying. +# +# Notes: +# +# Multiple actionsfile lines are permitted, and are in fact +# recommended! +# +# The default values are default.action, which is the "main" +# actions file maintained by the developers, and user.action, +# where you can make your personal additions. +# +# Actions files contain all the per site and per URL +# configuration for ad blocking, cookie management, privacy +# considerations, etc. +# +actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on. +actionsfile default.action # Main actions file +actionsfile user.action # User customizations +# +# 2.6. filterfile +# ================ +# +# Specifies: +# +# The filter file(s) to use +# +# Type of value: +# +# File name, relative to confdir +# +# Default value: +# +# default.filter (Unix) or default.filter.txt (Windows) +# +# Effect if unset: +# +# No textual content filtering takes place, i.e. all +filter{name} +# actions in the actions files are turned neutral. +# +# Notes: +# +# Multiple filterfile lines are permitted. +# +# The filter files contain content modification rules that use +# regular expressions. These rules permit powerful changes on +# the content of Web pages, and optionally the headers as well, +# e.g., you could try to disable your favorite JavaScript +# annoyances, re-write the actual displayed text, or just have +# some fun playing buzzword bingo with web pages. +# +# The +filter{name} actions rely on the relevant filter (name) +# to be defined in a filter file! +# +# A pre-defined filter file called default.filter that contains +# a number of useful filters for common problems is included in +# the distribution. See the section on the filter action for a +# list. +# +# It is recommended to place any locally adapted filters into a +# separate file, such as user.filter. +# +filterfile default.filter +filterfile user.filter # User customizations +# +# 2.7. logfile +# ============= +# +# Specifies: +# +# The log file to use +# +# Type of value: +# +# File name, relative to logdir +# +# Default value: +# +# Unset (commented out). When activated: logfile (Unix) or +# privoxy.log (Windows). +# +# Effect if unset: +# +# No logfile is written. +# +# Notes: +# +# The logfile is where all logging and error messages are +# written. The level of detail and number of messages are set +# with the debug option (see below). The logfile can be useful +# for tracking down a problem with Privoxy (e.g., it's not +# blocking an ad you think it should block) and it can help you +# to monitor what your browser is doing. +# +# Depending on the debug options below, the logfile may be a +# privacy risk if third parties can get access to it. As most +# users will never look at it, Privoxy only logs fatal errors by +# default. +# +# For most troubleshooting purposes, you will have to change +# that, please refer to the debugging section for details. +# +# Any log files must be writable by whatever user Privoxy is +# being run as (on Unix, default user id is "privoxy"). +# +# To prevent the logfile from growing indefinitely, it is +# recommended to periodically rotate or shorten it. Many +# operating systems support log rotation out of the box, some +# require additional software to do it. For details, please +# refer to the documentation for your operating system. +# +logfile logfile +# +# 2.8. trustfile +# =============== +# +# Specifies: +# +# The name of the trust file to use +# +# Type of value: +# +# File name, relative to confdir +# +# Default value: +# +# Unset (commented out). When activated: trust (Unix) or +# trust.txt (Windows) +# +# Effect if unset: +# +# The entire trust mechanism is disabled. +# +# Notes: +# +# The trust mechanism is an experimental feature for building +# white-lists and should be used with care. It is NOT +# recommended for the casual user. +# +# If you specify a trust file, Privoxy will only allow access to +# sites that are specified in the trustfile. Sites can be listed +# in one of two ways: +# +# Prepending a ~ character limits access to this site only (and +# any sub-paths within this site), e.g. ~www.example.com allows +# access to ~www.example.com/features/news.html, etc. +# +# Or, you can designate sites as trusted referrers, by +# prepending the name with a + character. The effect is that +# access to untrusted sites will be granted -- but only if a +# link from this trusted referrer was used to get there. The +# link target will then be added to the "trustfile" so that +# future, direct accesses will be granted. Sites added via this +# mechanism do not become trusted referrers themselves (i.e. +# they are added with a ~ designation). There is a limit of 512 +# such entries, after which new entries will not be made. +# +# If you use the + operator in the trust file, it may grow +# considerably over time. +# +# It is recommended that Privoxy be compiled with the +# --disable-force, --disable-toggle and --disable-editor +# options, if this feature is to be used. +# +# Possible applications include limiting Internet access for +# children. +# +#trustfile trust +# +# 3. DEBUGGING +# ============= +# +# These options are mainly useful when tracing a problem. Note that +# you might also want to invoke Privoxy with the --no-daemon command +# line option when debugging. +# +# +# 3.1. debug +# =========== +# +# Specifies: +# +# Key values that determine what information gets logged. +# +# Type of value: +# +# Integer values +# +# Default value: +# +# 0 (i.e.: only fatal errors (that cause Privoxy to exit) are +# logged) +# +# Effect if unset: +# +# Default value is used (see above). +# +# Notes: +# +# The available debug levels are: +# +# debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. +# debug 2 # show each connection status +# debug 4 # show I/O status +# debug 8 # show header parsing +# debug 16 # log all data written to the network +# debug 32 # debug force feature +# debug 64 # debug regular expression filters +# debug 128 # debug redirects +# debug 256 # debug GIF de-animation +# debug 512 # Common Log Format +# debug 1024 # Log the destination for requests Privoxy didn't let through, and the reason why. +# debug 2048 # CGI user interface +# debug 4096 # Startup banner and warnings. +# debug 8192 # Non-fatal errors +# debug 32768 # log all data read from the network +# debug 65536 # Log the applying actions +# +# To select multiple debug levels, you can either add them or +# use multiple debug lines. +# +# A debug level of 1 is informative because it will show you +# each request as it happens. 1, 1024, 4096 and 8192 are +# recommended so that you will notice when things go wrong. The +# other levels are probably only of interest if you are hunting +# down a specific problem. They can produce a hell of an output +# (especially 16). +# +# If you are used to the more verbose settings, simply enable +# the debug lines below again. +# +# If you want to use pure CLF (Common Log Format), you should +# set "debug 512" ONLY and not enable anything else. +# +# Privoxy has a hard-coded limit for the length of log messages. +# If it's reached, messages are logged truncated and marked with +# "... [too long, truncated]". +# +# Please don't file any support requests without trying to +# reproduce the problem with increased debug level first. Once +# you read the log messages, you may even be able to solve the +# problem on your own. +# +#debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. +#debug 1024 # Actions that are applied to all sites and maybe overruled later on. +#debug 4096 # Startup banner and warnings +#debug 8192 # Non-fatal errors +# +# 3.2. single-threaded +# ===================== +# +# Specifies: +# +# Whether to run only one server thread. +# +# Type of value: +# +# 1 or 0 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Multi-threaded (or, where unavailable: forked) operation, i.e. +# the ability to serve multiple requests simultaneously. +# +# Notes: +# +# This option is only there for debugging purposes. It will +# drastically reduce performance. +# +#single-threaded 1 +# +# 3.3. hostname +# ============== +# +# Specifies: +# +# The hostname shown on the CGI pages. +# +# Type of value: +# +# Text +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# The hostname provided by the operating system is used. +# +# Notes: +# +# On some misconfigured systems resolving the hostname fails or +# takes too much time and slows Privoxy down. Setting a fixed +# hostname works around the problem. +# +# In other circumstances it might be desirable to show a +# hostname other than the one returned by the operating system. +# For example if the system has several different hostnames and +# you don't want to use the first one. +# +# Note that Privoxy does not validate the specified hostname +# value. +# +#hostname hostname.example.org +# +# 4. ACCESS CONTROL AND SECURITY +# =============================== +# +# This section of the config file controls the security-relevant +# aspects of Privoxy's configuration. +# +# +# 4.1. listen-address +# ==================== +# +# Specifies: +# +# The address and TCP port on which Privoxy will listen for +# client requests. +# +# Type of value: +# +# [IP-Address]:Port +# +# [Hostname]:Port +# +# Default value: +# +# 127.0.0.1:8118 +# +# Effect if unset: +# +# Bind to 127.0.0.1 (IPv4 localhost), port 8118. This is +# suitable and recommended for home users who run Privoxy on the +# same machine as their browser. +# +# Notes: +# +# You will need to configure your browser(s) to this proxy +# address and port. +# +# If you already have another service running on port 8118, or +# if you want to serve requests from other machines (e.g. on +# your local network) as well, you will need to override the +# default. +# +# You can use this statement multiple times to make Privoxy +# listen on more ports or more IP addresses. Suitable if your +# operating system does not support sharing IPv6 and IPv4 +# protocols on the same socket. +# +# If a hostname is used instead of an IP address, Privoxy will +# try to resolve it to an IP address and if there are multiple, +# use the first one returned. +# +# If the address for the hostname isn't already known on the +# system (for example because it's in /etc/hostname), this may +# result in DNS traffic. +# +# If the specified address isn't available on the system, or if +# the hostname can't be resolved, Privoxy will fail to start. +# +# IPv6 addresses containing colons have to be quoted by +# brackets. They can only be used if Privoxy has been compiled +# with IPv6 support. If you aren't sure if your version supports +# it, have a look at http://config.privoxy.org/show-status. +# +# Some operating systems will prefer IPv6 to IPv4 addresses even +# if the system has no IPv6 connectivity which is usually not +# expected by the user. Some even rely on DNS to resolve +# localhost which mean the "localhost" address used may not +# actually be local. +# +# It is therefore recommended to explicitly configure the +# intended IP address instead of relying on the operating +# system, unless there's a strong reason not to. +# +# If you leave out the address, Privoxy will bind to all IPv4 +# interfaces (addresses) on your machine and may become +# reachable from the Internet and/or the local network. Be aware +# that some GNU/Linux distributions modify that behaviour +# without updating the documentation. Check for non-standard +# patches if your Privoxy version behaves differently. +# +# If you configure Privoxy to be reachable from the network, +# consider using access control lists (ACL's, see below), and/or +# a firewall. +# +# If you open Privoxy to untrusted users, you will also want to +# make sure that the following actions are disabled: +# enable-edit-actions and enable-remote-toggle +# +# Example: +# +# Suppose you are running Privoxy on a machine which has the +# address 192.168.0.1 on your local private network +# (192.168.0.0) and has another outside connection with a +# different address. You want it to serve requests from inside +# only: +# +# listen-address 192.168.0.1:8118 +# +# Suppose you are running Privoxy on an IPv6-capable machine and +# you want it to listen on the IPv6 address of the loopback +# device: +# +# listen-address [::1]:8118 +# +# +listen-address 172.16.0.1:8118 +# +# 4.2. toggle +# ============ +# +# Specifies: +# +# Initial state of "toggle" status +# +# Type of value: +# +# 1 or 0 +# +# Default value: +# +# 1 +# +# Effect if unset: +# +# Act as if toggled on +# +# Notes: +# +# If set to 0, Privoxy will start in "toggled off" mode, i.e. +# mostly behave like a normal, content-neutral proxy with both +# ad blocking and content filtering disabled. See +# enable-remote-toggle below. +# +toggle 1 +# +# 4.3. enable-remote-toggle +# ========================== +# +# Specifies: +# +# Whether or not the web-based toggle feature may be used +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The web-based toggle feature is disabled. +# +# Notes: +# +# When toggled off, Privoxy mostly acts like a normal, +# content-neutral proxy, i.e. doesn't block ads or filter +# content. +# +# Access to the toggle feature can not be controlled separately +# by "ACLs" or HTTP authentication, so that everybody who can +# access Privoxy (see "ACLs" and listen-address above) can +# toggle it for all users. So this option is not recommended for +# multi-user environments with untrusted users. +# +# Note that malicious client side code (e.g Java) is also +# capable of using this option. +# +# As a lot of Privoxy users don't read documentation, this +# feature is disabled by default. +# +# Note that you must have compiled Privoxy with support for this +# feature, otherwise this option has no effect. +# +enable-remote-toggle 0 +# +# 4.4. enable-remote-http-toggle +# =============================== +# +# Specifies: +# +# Whether or not Privoxy recognizes special HTTP headers to +# change its behaviour. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy ignores special HTTP headers. +# +# Notes: +# +# When toggled on, the client can change Privoxy's behaviour by +# setting special HTTP headers. Currently the only supported +# special header is "X-Filter: No", to disable filtering for the +# ongoing request, even if it is enabled in one of the action +# files. +# +# This feature is disabled by default. If you are using Privoxy +# in a environment with trusted clients, you may enable this +# feature at your discretion. Note that malicious client side +# code (e.g Java) is also capable of using this feature. +# +# This option will be removed in future releases as it has been +# obsoleted by the more general header taggers. +# +enable-remote-http-toggle 0 +# +# 4.5. enable-edit-actions +# ========================= +# +# Specifies: +# +# Whether or not the web-based actions file editor may be used +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The web-based actions file editor is disabled. +# +# Notes: +# +# Access to the editor can not be controlled separately by +# "ACLs" or HTTP authentication, so that everybody who can +# access Privoxy (see "ACLs" and listen-address above) can +# modify its configuration for all users. +# +# This option is not recommended for environments with untrusted +# users and as a lot of Privoxy users don't read documentation, +# this feature is disabled by default. +# +# Note that malicious client side code (e.g Java) is also +# capable of using the actions editor and you shouldn't enable +# this options unless you understand the consequences and are +# sure your browser is configured correctly. +# +# Note that you must have compiled Privoxy with support for this +# feature, otherwise this option has no effect. +# +enable-edit-actions 0 +# +# 4.6. enforce-blocks +# ==================== +# +# Specifies: +# +# Whether the user is allowed to ignore blocks and can "go there +# anyway". +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Blocks are not enforced. +# +# Notes: +# +# Privoxy is mainly used to block and filter requests as a +# service to the user, for example to block ads and other junk +# that clogs the pipes. Privoxy's configuration isn't perfect +# and sometimes innocent pages are blocked. In this situation it +# makes sense to allow the user to enforce the request and have +# Privoxy ignore the block. +# +# In the default configuration Privoxy's "Blocked" page contains +# a "go there anyway" link to adds a special string (the force +# prefix) to the request URL. If that link is used, Privoxy will +# detect the force prefix, remove it again and let the request +# pass. +# +# Of course Privoxy can also be used to enforce a network +# policy. In that case the user obviously should not be able to +# bypass any blocks, and that's what the "enforce-blocks" option +# is for. If it's enabled, Privoxy hides the "go there anyway" +# link. If the user adds the force prefix by hand, it will not +# be accepted and the circumvention attempt is logged. +# +# Examples: +# +# enforce-blocks 1 +# +enforce-blocks 0 +# +# 4.7. ACLs: permit-access and deny-access +# ========================================= +# +# Specifies: +# +# Who can access what. +# +# Type of value: +# +# src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]] +# +# Where src_addr and dst_addr are IPv4 addresses in dotted +# decimal notation or valid DNS names, port is a port number, +# and src_masklen and dst_masklen are subnet masks in CIDR +# notation, i.e. integer values from 2 to 30 representing the +# length (in bits) of the network address. The masks and the +# whole destination part are optional. +# +# If your system implements RFC 3493, then src_addr and dst_addr +# can be IPv6 addresses delimeted by brackets, port can be a +# number or a service name, and src_masklen and dst_masklen can +# be a number from 0 to 128. +# +# Default value: +# +# Unset +# +# If no port is specified, any port will match. If no +# src_masklen or src_masklen is given, the complete IP address +# has to match (i.e. 32 bits for IPv4 and 128 bits for IPv6). +# +# Effect if unset: +# +# Don't restrict access further than implied by listen-address +# +# Notes: +# +# Access controls are included at the request of ISPs and +# systems administrators, and are not usually needed by +# individual users. For a typical home user, it will normally +# suffice to ensure that Privoxy only listens on the localhost +# (127.0.0.1) or internal (home) network address by means of the +# listen-address option. +# +# Please see the warnings in the FAQ that Privoxy is not +# intended to be a substitute for a firewall or to encourage +# anyone to defer addressing basic security weaknesses. +# +# Multiple ACL lines are OK. If any ACLs are specified, Privoxy +# only talks to IP addresses that match at least one +# permit-access line and don't match any subsequent deny-access +# line. In other words, the last match wins, with the default +# being deny-access. +# +# If Privoxy is using a forwarder (see forward below) for a +# particular destination URL, the dst_addr that is examined is +# the address of the forwarder and NOT the address of the +# ultimate target. This is necessary because it may be +# impossible for the local Privoxy to determine the IP address +# of the ultimate target (that's often what gateways are used +# for). +# +# You should prefer using IP addresses over DNS names, because +# the address lookups take time. All DNS names must resolve! You +# can not use domain patterns like "*.org" or partial domain +# names. If a DNS name resolves to multiple IP addresses, only +# the first one is used. +# +# Some systems allow IPv4 clients to connect to IPv6 server +# sockets. Then the client's IPv4 address will be translated by +# the system into IPv6 address space with special prefix +# ::ffff:0:0/96 (so called IPv4 mapped IPv6 address). Privoxy +# can handle it and maps such ACL addresses automatically. +# +# Denying access to particular sites by ACL may have undesired +# side effects if the site in question is hosted on a machine +# which also hosts other sites (most sites are). +# +# Examples: +# +# Explicitly define the default behavior if no ACL and +# listen-address are set: "localhost" is OK. The absence of a +# dst_addr implies that all destination addresses are OK: +# +# permit-access localhost +# +# Allow any host on the same class C subnet as www.privoxy.org +# access to nothing but www.example.com (or other domains hosted +# on the same system): +# +# permit-access www.privoxy.org/24 www.example.com/32 +# +# Allow access from any host on the 26-bit subnet 192.168.45.64 +# to anywhere, with the exception that 192.168.45.73 may not +# access the IP address behind www.dirty-stuff.example.com: +# +# permit-access 192.168.45.64/26 +# deny-access 192.168.45.73 www.dirty-stuff.example.com +# +# Allow access from the IPv4 network 192.0.2.0/24 even if +# listening on an IPv6 wild card address (not supported on all +# platforms): +# +# permit-access 192.0.2.0/24 +# +# This is equivalent to the following line even if listening on +# an IPv4 address (not supported on all platforms): +# +# permit-access [::ffff:192.0.2.0]/120 +# +# +# 4.8. buffer-limit +# ================== +# +# Specifies: +# +# Maximum size of the buffer for content filtering. +# +# Type of value: +# +# Size in Kbytes +# +# Default value: +# +# 4096 +# +# Effect if unset: +# +# Use a 4MB (4096 KB) limit. +# +# Notes: +# +# For content filtering, i.e. the +filter and +deanimate-gif +# actions, it is necessary that Privoxy buffers the entire +# document body. This can be potentially dangerous, since a +# server could just keep sending data indefinitely and wait for +# your RAM to exhaust -- with nasty consequences. Hence this +# option. +# +# When a document buffer size reaches the buffer-limit, it is +# flushed to the client unfiltered and no further attempt to +# filter the rest of the document is made. Remember that there +# may be multiple threads running, which might require up to +# buffer-limit Kbytes each, unless you have enabled +# "single-threaded" above. +# +buffer-limit 4096 +# +# 4.9. enable-proxy-authentication-forwarding +# ============================================ +# +# Specifies: +# +# Whether or not proxy authentication through Privoxy should +# work. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Proxy authentication headers are removed. +# +# Notes: +# +# Privoxy itself does not support proxy authentication, but can +# allow clients to authenticate against Privoxy's parent proxy. +# +# By default Privoxy (3.0.21 and later) don't do that and remove +# Proxy-Authorization headers in requests and Proxy-Authenticate +# headers in responses to make it harder for malicious sites to +# trick inexperienced users into providing login information. +# +# If this option is enabled the headers are forwarded. +# +# Enabling this option is not recommended if there is no parent +# proxy that requires authentication or if the local network +# between Privoxy and the parent proxy isn't trustworthy. If +# proxy authentication is only required for some requests, it is +# recommended to use a client header filter to remove the +# authentication headers for requests where they aren't needed. +# +enable-proxy-authentication-forwarding 0 +# +# 5. FORWARDING +# ============== +# +# This feature allows routing of HTTP requests through a chain of +# multiple proxies. +# +# Forwarding can be used to chain Privoxy with a caching proxy to +# speed up browsing. Using a parent proxy may also be necessary if +# the machine that Privoxy runs on has no direct Internet access. +# +# Note that parent proxies can severely decrease your privacy level. +# For example a parent proxy could add your IP address to the +# request headers and if it's a caching proxy it may add the "Etag" +# header to revalidation requests again, even though you configured +# Privoxy to remove it. It may also ignore Privoxy's header time +# randomization and use the original values which could be used by +# the server as cookie replacement to track your steps between +# visits. +# +# Also specified here are SOCKS proxies. Privoxy supports the SOCKS +# 4 and SOCKS 4A protocols. +# +# +# 5.1. forward +# ============= +# +# Specifies: +# +# To which parent HTTP proxy specific requests should be routed. +# +# Type of value: +# +# target_pattern http_parent[:port] +# +# where target_pattern is a URL pattern that specifies to which +# requests (i.e. URLs) this forward rule shall apply. Use / to +# denote "all URLs". http_parent[:port] is the DNS name or IP +# address of the parent HTTP proxy through which the requests +# should be forwarded, optionally followed by its listening port +# (default: 8000). Use a single dot (.) to denote "no +# forwarding". +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# Don't use parent HTTP proxies. +# +# Notes: +# +# If http_parent is ".", then requests are not forwarded to +# another HTTP proxy but are made directly to the web servers. +# +# http_parent can be a numerical IPv6 address (if RFC 3493 is +# implemented). To prevent clashes with the port delimiter, the +# whole IP address has to be put into brackets. On the other +# hand a target_pattern containing an IPv6 address has to be put +# into angle brackets (normal brackets are reserved for regular +# expressions already). +# +# Multiple lines are OK, they are checked in sequence, and the +# last match wins. +# +# Examples: +# +# Everything goes to an example parent proxy, except SSL on port +# 443 (which it doesn't handle): +# +# forward / parent-proxy.example.org:8080 +# forward :443 . +# +# Everything goes to our example ISP's caching proxy, except for +# requests to that ISP's sites: +# +# forward / caching-proxy.isp.example.net:8000 +# forward .isp.example.net . +# +# Parent proxy specified by an IPv6 address: +# +# forward / [2001:DB8::1]:8000 +# +# Suppose your parent proxy doesn't support IPv6: +# +# forward / parent-proxy.example.org:8000 +# forward ipv6-server.example.org . +# forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . +forward / 172.16.0.1:8080 +forward :443 . +# +# +# 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t +# ========================================================================= +# +# Specifies: +# +# Through which SOCKS proxy (and optionally to which parent HTTP +# proxy) specific requests should be routed. +# +# Type of value: +# +# target_pattern socks_proxy[:port] http_parent[:port] +# +# where target_pattern is a URL pattern that specifies to which +# requests (i.e. URLs) this forward rule shall apply. Use / to +# denote "all URLs". http_parent and socks_proxy are IP +# addresses in dotted decimal notation or valid DNS names ( +# http_parent may be "." to denote "no HTTP forwarding"), and +# the optional port parameters are TCP ports, i.e. integer +# values from 1 to 65535 +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# Don't use SOCKS proxies. +# +# Notes: +# +# Multiple lines are OK, they are checked in sequence, and the +# last match wins. +# +# The difference between forward-socks4 and forward-socks4a is +# that in the SOCKS 4A protocol, the DNS resolution of the +# target hostname happens on the SOCKS server, while in SOCKS 4 +# it happens locally. +# +# With forward-socks5 the DNS resolution will happen on the +# remote server as well. +# +# forward-socks5t works like vanilla forward-socks5 but lets +# Privoxy additionally use Tor-specific SOCKS extensions. +# Currently the only supported SOCKS extension is optimistic +# data which can reduce the latency for the first request made +# on a newly created connection. +# +# socks_proxy and http_parent can be a numerical IPv6 address +# (if RFC 3493 is implemented). To prevent clashes with the port +# delimiter, the whole IP address has to be put into brackets. +# On the other hand a target_pattern containing an IPv6 address +# has to be put into angle brackets (normal brackets are +# reserved for regular expressions already). +# +# If http_parent is ".", then requests are not forwarded to +# another HTTP proxy but are made (HTTP-wise) directly to the +# web servers, albeit through a SOCKS proxy. +# +# Examples: +# +# From the company example.com, direct connections are made to +# all "internal" domains, but everything outbound goes through +# their ISP's proxy by way of example.com's corporate SOCKS 4A +# gateway to the Internet. +# +# forward-socks4a / socks-gw.example.com:1080 www-cache.isp.example.net:8080 +# forward .example.com . +# +# A rule that uses a SOCKS 4 gateway for all destinations but no +# HTTP parent looks like this: +# +# forward-socks4 / socks-gw.example.com:1080 . +# +# To chain Privoxy and Tor, both running on the same system, you +# would use something like: +# +# forward-socks5t / 127.0.0.1:9050 . +# +# Note that if you got Tor through one of the bundles, you may +# have to change the port from 9050 to 9150 (or even another +# one). For details, please check the documentation on the Tor +# website. +# +# The public Tor network can't be used to reach your local +# network, if you need to access local servers you therefore +# might want to make some exceptions: +# +# forward 192.168.*.*/ . +# forward 10.*.*.*/ . +# forward 127.*.*.*/ . +# +# Unencrypted connections to systems in these address ranges +# will be as (un)secure as the local network is, but the +# alternative is that you can't reach the local network through +# Privoxy at all. Of course this may actually be desired and +# there is no reason to make these exceptions if you aren't sure +# you need them. +# +# If you also want to be able to reach servers in your local +# network by using their names, you will need additional +# exceptions that look like this: +# +# forward localhost/ . +# +# +# 5.3. forwarded-connect-retries +# =============================== +# +# Specifies: +# +# How often Privoxy retries if a forwarded connection request +# fails. +# +# Type of value: +# +# Number of retries. +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Connections forwarded through other proxies are treated like +# direct connections and no retry attempts are made. +# +# Notes: +# +# forwarded-connect-retries is mainly interesting for socks4a +# connections, where Privoxy can't detect why the connections +# failed. The connection might have failed because of a DNS +# timeout in which case a retry makes sense, but it might also +# have failed because the server doesn't exist or isn't +# reachable. In this case the retry will just delay the +# appearance of Privoxy's error message. +# +# Note that in the context of this option, "forwarded +# connections" includes all connections that Privoxy forwards +# through other proxies. This option is not limited to the HTTP +# CONNECT method. +# +# Only use this option, if you are getting lots of +# forwarding-related error messages that go away when you try +# again manually. Start with a small value and check Privoxy's +# logfile from time to time, to see how many retries are usually +# needed. +# +# Examples: +# +# forwarded-connect-retries 1 +# +forwarded-connect-retries 0 +# +# 6. MISCELLANEOUS +# ================= +# +# 6.1. accept-intercepted-requests +# ================================= +# +# Specifies: +# +# Whether intercepted requests should be treated as valid. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Only proxy requests are accepted, intercepted requests are +# treated as invalid. +# +# Notes: +# +# If you don't trust your clients and want to force them to use +# Privoxy, enable this option and configure your packet filter +# to redirect outgoing HTTP connections into Privoxy. +# +# Note that intercepting encrypted connections (HTTPS) isn't +# supported. +# +# Make sure that Privoxy's own requests aren't redirected as +# well. Additionally take care that Privoxy can't intentionally +# connect to itself, otherwise you could run into redirection +# loops if Privoxy's listening port is reachable by the outside +# or an attacker has access to the pages you visit. +# +# Examples: +# +# accept-intercepted-requests 1 +# +accept-intercepted-requests 0 +# +# 6.2. allow-cgi-request-crunching +# ================================= +# +# Specifies: +# +# Whether requests to Privoxy's CGI pages can be blocked or +# redirected. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy ignores block and redirect actions for its CGI pages. +# +# Notes: +# +# By default Privoxy ignores block or redirect actions for its +# CGI pages. Intercepting these requests can be useful in +# multi-user setups to implement fine-grained access control, +# but it can also render the complete web interface useless and +# make debugging problems painful if done without care. +# +# Don't enable this option unless you're sure that you really +# need it. +# +# Examples: +# +# allow-cgi-request-crunching 1 +# +allow-cgi-request-crunching 0 +# +# 6.3. split-large-forms +# ======================= +# +# Specifies: +# +# Whether the CGI interface should stay compatible with broken +# HTTP clients. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The CGI form generate long GET URLs. +# +# Notes: +# +# Privoxy's CGI forms can lead to rather long URLs. This isn't a +# problem as far as the HTTP standard is concerned, but it can +# confuse clients with arbitrary URL length limitations. +# +# Enabling split-large-forms causes Privoxy to divide big forms +# into smaller ones to keep the URL length down. It makes +# editing a lot less convenient and you can no longer submit all +# changes at once, but at least it works around this browser +# bug. +# +# If you don't notice any editing problems, there is no reason +# to enable this option, but if one of the submit buttons +# appears to be broken, you should give it a try. +# +# Examples: +# +# split-large-forms 1 +# +split-large-forms 0 +# +# 6.4. keep-alive-timeout +# ======================== +# +# Specifies: +# +# Number of seconds after which an open connection will no +# longer be reused. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections are not kept alive. +# +# Notes: +# +# This option allows clients to keep the connection to Privoxy +# alive. If the server supports it, Privoxy will keep the +# connection to the server alive as well. Under certain +# circumstances this may result in speed-ups. +# +# By default, Privoxy will close the connection to the server if +# the client connection gets closed, or if the specified timeout +# has been reached without a new request coming in. This +# behaviour can be changed with the connection-sharing option. +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support. +# +# Note that a timeout of five seconds as used in the default +# configuration file significantly decreases the number of +# connections that will be reused. The value is used because +# some browsers limit the number of connections they open to a +# single host and apply the same limit to proxies. This can +# result in a single website "grabbing" all the connections the +# browser allows, which means connections to other websites +# can't be opened until the connections currently in use time +# out. +# +# Several users have reported this as a Privoxy bug, so the +# default value has been reduced. Consider increasing it to 300 +# seconds or even more if you think your browser can handle it. +# If your browser appears to be hanging, it probably can't. +# +# Examples: +# +# keep-alive-timeout 300 +# +keep-alive-timeout 5 +# +# 6.5. tolerate-pipelining +# ========================= +# +# Specifies: +# +# Whether or not pipelined requests should be served. +# +# Type of value: +# +# 0 or 1. +# +# Default value: +# +# None +# +# Effect if unset: +# +# If Privoxy receives more than one request at once, it +# terminates the client connection after serving the first one. +# +# Notes: +# +# Privoxy currently doesn't pipeline outgoing requests, thus +# allowing pipelining on the client connection is not guaranteed +# to improve the performance. +# +# By default Privoxy tries to discourage clients from pipelining +# by discarding aggressively pipelined requests, which forces +# the client to resend them through a new connection. +# +# This option lets Privoxy tolerate pipelining. Whether or not +# that improves performance mainly depends on the client +# configuration. +# +# If you are seeing problems with pages not properly loading, +# disabling this option could work around the problem. +# +# Examples: +# +# tolerate-pipelining 1 +# +tolerate-pipelining 1 +# +# 6.6. default-server-timeout +# ============================ +# +# Specifies: +# +# Assumed server-side keep-alive timeout if not specified by the +# server. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections for which the server didn't specify the keep-alive +# timeout are not reused. +# +# Notes: +# +# Enabling this option significantly increases the number of +# connections that are reused, provided the keep-alive-timeout +# option is also enabled. +# +# While it also increases the number of connections problems +# when Privoxy tries to reuse a connection that already has been +# closed on the server side, or is closed while Privoxy is +# trying to reuse it, this should only be a problem if it +# happens for the first request sent by the client. If it +# happens for requests on reused client connections, Privoxy +# will simply close the connection and the client is supposed to +# retry the request without bothering the user. +# +# Enabling this option is therefore only recommended if the +# connection-sharing option is disabled. +# +# It is an error to specify a value larger than the +# keep-alive-timeout value. +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support. +# +# Examples: +# +# default-server-timeout 60 +# +#default-server-timeout 60 +# +# 6.7. connection-sharing +# ======================== +# +# Specifies: +# +# Whether or not outgoing connections that have been kept alive +# should be shared between different incoming connections. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections are not shared. +# +# Notes: +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support, or if it's disabled. +# +# Notes: +# +# Note that reusing connections doesn't necessary cause +# speedups. There are also a few privacy implications you should +# be aware of. +# +# If this option is effective, outgoing connections are shared +# between clients (if there are more than one) and closing the +# browser that initiated the outgoing connection does no longer +# affect the connection between Privoxy and the server unless +# the client's request hasn't been completed yet. +# +# If the outgoing connection is idle, it will not be closed +# until either Privoxy's or the server's timeout is reached. +# While it's open, the server knows that the system running +# Privoxy is still there. +# +# If there are more than one client (maybe even belonging to +# multiple users), they will be able to reuse each others +# connections. This is potentially dangerous in case of +# authentication schemes like NTLM where only the connection is +# authenticated, instead of requiring authentication for each +# request. +# +# If there is only a single client, and if said client can keep +# connections alive on its own, enabling this option has next to +# no effect. If the client doesn't support connection +# keep-alive, enabling this option may make sense as it allows +# Privoxy to keep outgoing connections alive even if the client +# itself doesn't support it. +# +# You should also be aware that enabling this option increases +# the likelihood of getting the "No server or forwarder data" +# error message, especially if you are using a slow connection +# to the Internet. +# +# This option should only be used by experienced users who +# understand the risks and can weight them against the benefits. +# +# Examples: +# +# connection-sharing 1 +# +#connection-sharing 1 +# +# 6.8. socket-timeout +# ==================== +# +# Specifies: +# +# Number of seconds after which a socket times out if no data is +# received. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# A default value of 300 seconds is used. +# +# Notes: +# +# The default is quite high and you probably want to reduce it. +# If you aren't using an occasionally slow proxy like Tor, +# reducing it to a few seconds should be fine. +# +# Examples: +# +# socket-timeout 300 +# +socket-timeout 300 +# +# 6.9. max-client-connections +# ============================ +# +# Specifies: +# +# Maximum number of client connections that will be served. +# +# Type of value: +# +# Positive number. +# +# Default value: +# +# 128 +# +# Effect if unset: +# +# Connections are served until a resource limit is reached. +# +# Notes: +# +# Privoxy creates one thread (or process) for every incoming +# client connection that isn't rejected based on the access +# control settings. +# +# If the system is powerful enough, Privoxy can theoretically +# deal with several hundred (or thousand) connections at the +# same time, but some operating systems enforce resource limits +# by shutting down offending processes and their default limits +# may be below the ones Privoxy would require under heavy load. +# +# Configuring Privoxy to enforce a connection limit below the +# thread or process limit used by the operating system makes +# sure this doesn't happen. Simply increasing the operating +# system's limit would work too, but if Privoxy isn't the only +# application running on the system, you may actually want to +# limit the resources used by Privoxy. +# +# If Privoxy is only used by a single trusted user, limiting the +# number of client connections is probably unnecessary. If there +# are multiple possibly untrusted users you probably still want +# to additionally use a packet filter to limit the maximal +# number of incoming connections per client. Otherwise a +# malicious user could intentionally create a high number of +# connections to prevent other users from using Privoxy. +# +# Obviously using this option only makes sense if you choose a +# limit below the one enforced by the operating system. +# +# One most POSIX-compliant systems Privoxy can't properly deal +# with more than FD_SETSIZE file descriptors at the same time +# and has to reject connections if the limit is reached. This +# will likely change in a future version, but currently this +# limit can't be increased without recompiling Privoxy with a +# different FD_SETSIZE limit. +# +# Examples: +# +# max-client-connections 256 +# +#max-client-connections 256 +# +# 6.10. handle-as-empty-doc-returns-ok +# ===================================== +# +# Specifies: +# +# The status code Privoxy returns for pages blocked with +# +handle-as-empty-document. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy returns a status 403(forbidden) for all blocked pages. +# +# Effect if set: +# +# Privoxy returns a status 200(OK) for pages blocked with +# +handle-as-empty-document and a status 403(Forbidden) for all +# other blocked pages. +# +# Notes: +# +# This directive was added as a work-around for Firefox bug +# 492459: "Websites are no longer rendered if SSL requests for +# JavaScripts are blocked by a proxy." +# (https://bugzilla.mozilla.org/show_bug.cgi?id=492459), the bug +# has been fixed for quite some time, but this directive is also +# useful to make it harder for websites to detect whether or not +# resources are being blocked. +# +#handle-as-empty-doc-returns-ok 1 +# +# 6.11. enable-compression +# ========================= +# +# Specifies: +# +# Whether or not buffered content is compressed before delivery. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy does not compress buffered content. +# +# Effect if set: +# +# Privoxy compresses buffered content before delivering it to +# the client, provided the client supports it. +# +# Notes: +# +# This directive is only supported if Privoxy has been compiled +# with FEATURE_COMPRESSION, which should not to be confused with +# FEATURE_ZLIB. +# +# Compressing buffered content is mainly useful if Privoxy and +# the client are running on different systems. If they are +# running on the same system, enabling compression is likely to +# slow things down. If you didn't measure otherwise, you should +# assume that it does and keep this option disabled. +# +# Privoxy will not compress buffered content below a certain +# length. +# +#enable-compression 1 +# +# 6.12. compression-level +# ======================== +# +# Specifies: +# +# The compression level that is passed to the zlib library when +# compressing buffered content. +# +# Type of value: +# +# Positive number ranging from 0 to 9. +# +# Default value: +# +# 1 +# +# Notes: +# +# Compressing the data more takes usually longer than +# compressing it less or not compressing it at all. Which level +# is best depends on the connection between Privoxy and the +# client. If you can't be bothered to benchmark it for yourself, +# you should stick with the default and keep compression +# disabled. +# +# If compression is disabled, the compression level is +# irrelevant. +# +# Examples: +# +# # Best speed (compared to the other levels) +# compression-level 1 +# +# # Best compression +# compression-level 9 +# +# # No compression. Only useful for testing as the added header +# # slightly increases the amount of data that has to be sent. +# # If your benchmark shows that using this compression level +# # is superior to using no compression at all, the benchmark +# # is likely to be flawed. +# compression-level 0 +# +# +#compression-level 1 +# +# 6.13. client-header-order +# ========================== +# +# Specifies: +# +# The order in which client headers are sorted before forwarding +# them. +# +# Type of value: +# +# Client header names delimited by spaces or tabs +# +# Default value: +# +# None +# +# Notes: +# +# By default Privoxy leaves the client headers in the order they +# were sent by the client. Headers are modified in-place, new +# headers are added at the end of the already existing headers. +# +# The header order can be used to fingerprint client requests +# independently of other headers like the User-Agent. +# +# This directive allows to sort the headers differently to +# better mimic a different User-Agent. Client headers will be +# emitted in the order given, headers whose name isn't +# explicitly specified are added at the end. +# +# Note that sorting headers in an uncommon way will make +# fingerprinting actually easier. Encrypted headers are not +# affected by this directive. +# +#client-header-order Host \ +# Accept \ +# Accept-Language \ +# Accept-Encoding \ +# Proxy-Connection \ +# Referer \ +# Cookie \ +# DNT \ +# If-Modified-Since \ +# Cache-Control \ +# Content-Length \ +# Content-Type +# +# +# 7. WINDOWS GUI OPTIONS +# ======================= +# +# Privoxy has a number of options specific to the Windows GUI +# interface: +# +# +# +# If "activity-animation" is set to 1, the Privoxy icon will animate +# when "Privoxy" is active. To turn off, set to 0. +# +#activity-animation 1 +# +# +# +# If "log-messages" is set to 1, Privoxy copies log messages to the +# console window. The log detail depends on the debug directive. +# +#log-messages 1 +# +# +# +# If "log-buffer-size" is set to 1, the size of the log buffer, i.e. +# the amount of memory used for the log messages displayed in the +# console window, will be limited to "log-max-lines" (see below). +# +# Warning: Setting this to 0 will result in the buffer to grow +# infinitely and eat up all your memory! +# +#log-buffer-size 1 +# +# +# +# log-max-lines is the maximum number of lines held in the log +# buffer. See above. +# +#log-max-lines 200 +# +# +# +# If "log-highlight-messages" is set to 1, Privoxy will highlight +# portions of the log messages with a bold-faced font: +# +#log-highlight-messages 1 +# +# +# +# The font used in the console window: +# +#log-font-name Comic Sans MS +# +# +# +# Font size used in the console window: +# +#log-font-size 8 +# +# +# +# "show-on-task-bar" controls whether or not Privoxy will appear as +# a button on the Task bar when minimized: +# +#show-on-task-bar 0 +# +# +# +# If "close-button-minimizes" is set to 1, the Windows close button +# will minimize Privoxy instead of closing the program (close with +# the exit option on the File menu). +# +#close-button-minimizes 1 +# +# +# +# The "hide-console" option is specific to the MS-Win console +# version of Privoxy. If this option is used, Privoxy will +# disconnect from and hide the command console. +# +#hide-console +# +# +# diff --git a/roles/features/templates/usr.sbin.dnsmasq.j2 b/roles/features/templates/usr.sbin.dnsmasq.j2 new file mode 100644 index 00000000..9b2c34bd --- /dev/null +++ b/roles/features/templates/usr.sbin.dnsmasq.j2 @@ -0,0 +1,68 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2009 John Dong +# Copyright (C) 2010 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +@{TFTP_DIR}=/var/tftp /srv/tftpboot + +#include + +/usr/sbin/dnsmasq { + #include + #include + #include + + capability net_bind_service, + capability setgid, + capability setuid, + capability dac_override, + capability net_admin, # for DHCP server + capability net_raw, # for DHCP server ping checks + network inet raw, + + signal (receive) peer=/usr/sbin/libvirtd, + ptrace (readby) peer=/usr/sbin/libvirtd, + + /etc/dnsmasq.conf r, + /etc/dnsmasq.d/ r, + /etc/dnsmasq.d/* r, + /etc/ethers r, + /etc/NetworkManager/dnsmasq.d/ r, + /etc/NetworkManager/dnsmasq.d/* r, + /etc/block.hosts r, + + /usr/sbin/dnsmasq mr, + + /{,var/}run/*dnsmasq*.pid w, + /{,var/}run/dnsmasq-forwarders.conf r, + /{,var/}run/dnsmasq/ r, + /{,var/}run/dnsmasq/* rw, + + /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage + + # for the read-only TFTP server + @{TFTP_DIR}/ r, + @{TFTP_DIR}/** r, + + # libvirt config, lease and hosts files for dnsmasq + /var/lib/libvirt/dnsmasq/ r, + /var/lib/libvirt/dnsmasq/* r, + /var/lib/libvirt/dnsmasq/*.leases rw, + + # libvirt pid files for dnsmasq + /{,var/}run/libvirt/network/ r, + /{,var/}run/libvirt/network/*.pid rw, + + # NetworkManager integration + /{,var/}run/nm-dns-dnsmasq.conf r, + /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, + /{,var/}run/NetworkManager/dnsmasq.conf r, + /{,var/}run/NetworkManager/dnsmasq.pid w, + +} diff --git a/roles/features/templates/usr.sbin.privoxy.j2 b/roles/features/templates/usr.sbin.privoxy.j2 new file mode 100644 index 00000000..5f8d9ddf --- /dev/null +++ b/roles/features/templates/usr.sbin.privoxy.j2 @@ -0,0 +1,15 @@ +#include + +/usr/sbin/privoxy { + #include + #include + + capability setgid, + capability setuid, + + /etc/privoxy/* r, + /etc/privoxy/templates/* r, + /run/privoxy.pid w, + /var/log/privoxy/logfile w, + +} diff --git a/roles/logging/templates/audit.rules.j2 b/roles/logging/templates/audit.rules.j2 new file mode 100644 index 00000000..3464e2a1 --- /dev/null +++ b/roles/logging/templates/audit.rules.j2 @@ -0,0 +1,101 @@ +# This file contains the auditctl rules that are loaded +# whenever the audit daemon is started via the initscripts. +# The rules are simply the parameters that would be passed +# to auditctl. +# +# First rule - delete all +-D + +# Increase the buffers to survive stress events. +# Make this bigger for busy systems +-b 320 + +# Feel free to add below this line. See auditctl man page + +# Record Events That Modify Date and Time Information +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S clock_settime -k time-change +-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change +{% endif %} +-a always,exit -F arch=b32 -S clock_settime -k time-change +-a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change +-w /etc/localtime -p wa -k time-change + +# Record Events That Modify User/Group Information +-w /etc/group -p wa -k identity +-w /etc/passwd -p wa -k identity +-w /etc/gshadow -p wa -k identity +-w /etc/shadow -p wa -k identity +-w /etc/security/opasswd -p wa -k identity + +# Record Events That Modify the System's Network Environment +{% if ansible_architecture == "x86_64" %} +-a exit,always -F arch=b64 -S sethostname -S setdomainname -k system-locale +{% endif %} +-a exit,always -F arch=b32 -S sethostname -S setdomainname -k system-locale +-w /etc/issue -p wa -k system-locale +-w /etc/issue.net -p wa -k system-locale +-w /etc/hosts -p wa -k system-locale +-w /etc/network/interfaces -p wa -k system-locale + +# Collect Login and Logout Events +-w /var/log/faillog -p wa -k logins +-w /var/log/lastlog -p wa -k logins +-w /var/log/tallylog -p wa -k logins + +# Collect Session Initiation Information +-w /var/run/utmp -p wa -k session +-w /var/log/wtmp -p wa -k session +-w /var/log/btmp -p wa -k session + +# Collect Discretionary Access Control Permission Modification Events +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod +-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod +-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod +{% endif %} +-a always,exit -F arch=b32 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod +-a always,exit -F arch=b32 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod +-a always,exit -F arch=b32 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod + +# Collect Unsuccessful Unauthorized Access Attempts to Files +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access +-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access +{% endif %} +-a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access +-a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access + +# Collect Use of Privileged Commands +{% if privileged_programs is defined and privileged_programs.stdout_lines|length > 0 %} +{{ privileged_programs.stdout }} +{% endif %} + +# Collect Successful File System Mounts +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S mount -F auid>=500 -F auid!=4294967295 -k mounts +{% endif %} +-a always,exit -F arch=b32 -S mount -F auid>=500 -F auid!=4294967295 -k mounts + +# Collect File Deletion Events by User +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete +{% endif %} +-a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete + +# Collect Changes to System Administration Scope +-w /etc/sudoers -p wa -k scope + +# Collect System Administrator Actions (sudolog) +-w /var/log/sudo.log -p wa -k actions + +# Collect Kernel Module Loading and Unloading +{% if ansible_architecture == "x86_64" %} +-a always,exit -F arch=b64 -S init_module -S delete_module -k modules +{% endif %} +-a always,exit -F arch=b32 -S init_module -S delete_module -k modules +-w /sbin/insmod -p x -k modules +-w /sbin/rmmod -p x -k modules +-w /sbin/modprobe -p x -k modules + +-e 2 diff --git a/roles/logging/templates/auditd.conf.j2 b/roles/logging/templates/auditd.conf.j2 new file mode 100644 index 00000000..24aac738 --- /dev/null +++ b/roles/logging/templates/auditd.conf.j2 @@ -0,0 +1,32 @@ +# +# This file controls the configuration of the audit daemon +# + +log_file = /var/log/audit/audit.log +log_format = RAW +log_group = root +priority_boost = 4 +flush = INCREMENTAL +freq = 20 +num_logs = 5 +disp_qos = lossy +dispatcher = /sbin/audispd +name_format = NONE +##name = mydomain +max_log_file = 10 +max_log_file_action = keep_logs +space_left = 75 +space_left_action = email +action_mail_acct = {{ auditd_action_mail_acct }} +admin_space_left = 50 +admin_space_left_action = email +disk_full_action = SUSPEND +disk_error_action = SUSPEND +##tcp_listen_port = +tcp_listen_queue = 5 +tcp_max_per_addr = 1 +##tcp_client_ports = 1024-65535 +tcp_client_max_idle = 0 +enable_krb5 = no +krb5_principal = auditd +##krb5_key_file = /etc/audit/audit.key \ No newline at end of file diff --git a/roles/security/templates/CIS.conf.j2 b/roles/security/templates/CIS.conf.j2 new file mode 100644 index 00000000..96b3a595 --- /dev/null +++ b/roles/security/templates/CIS.conf.j2 @@ -0,0 +1,15 @@ +*.emerg :omusrmsg:* +mail.* -/var/log/mail +mail.info -/var/log/mail.info +mail.warning -/var/log/mail.warn +mail.err /var/log/mail.err +news.crit -/var/log/news/news.crit +news.err -/var/log/news/news.err +news.notice -/var/log/news/news.notice +*.=warning;*.=err -/var/log/warn +*.crit /var/log/warn +*.*;mail.none;news.none -/var/log/messages +local0,local1.* -/var/log/localmessages +local2,local3.* -/var/log/localmessages +local4,local5.* -/var/log/localmessages +local6,local7.* -/var/log/localmessages \ No newline at end of file diff --git a/roles/security/templates/rsyslog.conf.j2 b/roles/security/templates/rsyslog.conf.j2 new file mode 100644 index 00000000..25513801 --- /dev/null +++ b/roles/security/templates/rsyslog.conf.j2 @@ -0,0 +1,61 @@ +# /etc/rsyslog.conf Configuration file for rsyslog. +# +# For more information see +# /usr/share/doc/rsyslog-doc/html/rsyslog_conf.html +# +# Default logging rules can be found in /etc/rsyslog.d/50-default.conf + +# +################# +#### MODULES #### +################# + +module(load="imuxsock") # provides support for local system logging +module(load="imklog") # provides kernel logging support +#module(load="immark") # provides --MARK-- message capability + +# provides UDP syslog reception +#module(load="imudp") +#input(type="imudp" port="514") + +# provides TCP syslog reception +#module(load="imtcp") +#input(type="imtcp" port="514") + +# Enable non-kernel facility klog messages +$KLogPermitNonKernelFacility on + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner syslog +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 +$PrivDropToUser syslog +$PrivDropToGroup syslog + +# +# Where to place spool and state files +# +$WorkDirectory /var/spool/rsyslog + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + diff --git a/roles/vpn/templates/easy-rsa.vars.j2 b/roles/vpn/templates/easy-rsa.vars.j2 new file mode 100644 index 00000000..f46993fb --- /dev/null +++ b/roles/vpn/templates/easy-rsa.vars.j2 @@ -0,0 +1,198 @@ +# Easy-RSA 3 parameter settings + +# NOTE: If you installed Easy-RSA from your distro's package manager, don't edit +# this file in place -- instead, you should copy the entire easy-rsa directory +# to another location so future upgrades don't wipe out your changes. + +# HOW TO USE THIS FILE +# +# vars.example contains built-in examples to Easy-RSA settings. You MUST name +# this file 'vars' if you want it to be used as a configuration file. If you do +# not, it WILL NOT be automatically read when you call easyrsa commands. +# +# It is not necessary to use this config file unless you wish to change +# operational defaults. These defaults should be fine for many uses without the +# need to copy and edit the 'vars' file. +# +# All of the editable settings are shown commented and start with the command +# 'set_var' -- this means any set_var command that is uncommented has been +# modified by the user. If you're happy with a default, there is no need to +# define the value to its default. + +# NOTES FOR WINDOWS USERS +# +# Paths for Windows *MUST* use forward slashes, or optionally double-esscaped +# backslashes (single forward slashes are recommended.) This means your path to +# the openssl binary might look like this: +# "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" + +# A little housekeeping: DON'T EDIT THIS SECTION +# +# Easy-RSA 3.x doesn't source into the environment directly. +# Complain if a user tries to do this: +if [ -z "$EASYRSA_CALLER" ]; then + echo "You appear to be sourcing an Easy-RSA 'vars' file." >&2 + echo "This is no longer necessary and is disallowed. See the section called" >&2 + echo "'How to use this file' near the top comments for more details." >&2 + return 1 +fi + +# DO YOUR EDITS BELOW THIS POINT + +# This variable should point to the top level of the easy-rsa tree. By default, +# this is taken to be the directory you are currently in. + +set_var EASYRSA "{{ easyrsa_dir }}/easyrsa3/" + +# If your OpenSSL command is not in the system PATH, you will need to define the +# path to it here. Normally this means a full path to the executable, otherwise +# you could have left it undefined here and the shown default would be used. +# +# Windows users, remember to use paths with forward-slashes (or escaped +# back-slashes.) Windows users should declare the full path to the openssl +# binary here if it is not in their system PATH. + +#set_var EASYRSA_OPENSSL "openssl" +# +# This sample is in Windows syntax -- edit it for your path if not using PATH: +#set_var EASYRSA_OPENSSL "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" + +# Edit this variable to point to your soon-to-be-created key directory. +# +# WARNING: init-pki will do a rm -rf on this directory so make sure you define +# it correctly! (Interactive mode will prompt before acting.) + +set_var EASYRSA_PKI "$EASYRSA/pki" + +# Define X509 DN mode. +# This is used to adjust what elements are included in the Subject field as the DN +# (this is the "Distinguished Name.") +# Note that in cn_only mode the Organizational fields further below aren't used. +# +# Choices are: +# cn_only - use just a CN value +# org - use the "traditional" Country/Province/City/Org/OU/email/CN format + +set_var EASYRSA_DN "cn_only" + +# Organizational fields (used with 'org' mode and ignored in 'cn_only' mode.) +# These are the default values for fields which will be placed in the +# certificate. Don't leave any of these fields blank, although interactively +# you may omit any specific field by typing the "." symbol (not valid for +# email.) + +#set_var EASYRSA_REQ_COUNTRY "US" +#set_var EASYRSA_REQ_PROVINCE "California" +#set_var EASYRSA_REQ_CITY "San Francisco" +#set_var EASYRSA_REQ_ORG "Copyleft Certificate Co" +#set_var EASYRSA_REQ_EMAIL "me@example.net" +#set_var EASYRSA_REQ_OU "My Organizational Unit" + +# Choose a size in bits for your keypairs. The recommended value is 2048. Using +# 2048-bit keys is considered more than sufficient for many years into the +# future. Larger keysizes will slow down TLS negotiation and make key/DH param +# generation take much longer. Values up to 4096 should be accepted by most +# software. Only used when the crypto alg is rsa (see below.) + +# set_var EASYRSA_KEY_SIZE 2048 + +# The default crypto mode is rsa; ec can enable elliptic curve support. +# Note that not all software supports ECC, so use care when enabling it. +# Choices for crypto alg are: (each in lower-case) +# * rsa +# * ec + +set_var EASYRSA_ALGO ec + +# Define the named curve, used in ec mode only: + +set_var EASYRSA_CURVE prime256v1 + +# In how many days should the root CA key expire? + +set_var EASYRSA_CA_EXPIRE {{ easyrsa_ca_expire }} + +# In how many days should certificates expire? + +set_var EASYRSA_CERT_EXPIRE {{ easyrsa_cert_expire }} + +# How many days until the next CRL publish date? Note that the CRL can still be +# parsed after this timeframe passes. It is only used for an expected next +# publication date. + +#set_var EASYRSA_CRL_DAYS 180 + +# Support deprecated "Netscape" extensions? (choices "yes" or "no".) The default +# is "no" to discourage use of deprecated extensions. If you require this +# feature to use with --ns-cert-type, set this to "yes" here. This support +# should be replaced with the more modern --remote-cert-tls feature. If you do +# not use --ns-cert-type in your configs, it is safe (and recommended) to leave +# this defined to "no". When set to "yes", server-signed certs get the +# nsCertType=server attribute, and also get any NS_COMMENT defined below in the +# nsComment field. + +#set_var EASYRSA_NS_SUPPORT "no" + +# When NS_SUPPORT is set to "yes", this field is added as the nsComment field. +# Set this blank to omit it. With NS_SUPPORT set to "no" this field is ignored. + +#set_var EASYRSA_NS_COMMENT "Easy-RSA Generated Certificate" + +# A temp file used to stage cert extensions during signing. The default should +# be fine for most users; however, some users might want an alternative under a +# RAM-based FS, such as /dev/shm or /tmp on some systems. + +#set_var EASYRSA_TEMP_FILE "$EASYRSA_PKI/extensions.temp" + +# !! +# NOTE: ADVANCED OPTIONS BELOW THIS POINT +# PLAY WITH THEM AT YOUR OWN RISK +# !! + +# Broken shell command aliases: If you have a largely broken shell that is +# missing any of these POSIX-required commands used by Easy-RSA, you will need +# to define an alias to the proper path for the command. The symptom will be +# some form of a 'command not found' error from your shell. This means your +# shell is BROKEN, but you can hack around it here if you really need. These +# shown values are not defaults: it is up to you to know what you're doing if +# you touch these. +# +#alias awk="/alt/bin/awk" +#alias cat="/alt/bin/cat" + +# X509 extensions directory: +# If you want to customize the X509 extensions used, set the directory to look +# for extensions here. Each cert type you sign must have a matching filename, +# and an optional file named 'COMMON' is included first when present. Note that +# when undefined here, default behaviour is to look in $EASYRSA_PKI first, then +# fallback to $EASYRSA for the 'x509-types' dir. You may override this +# detection with an explicit dir here. +# +#set_var EASYRSA_EXT_DIR "$EASYRSA/x509-types" + +# OpenSSL config file: +# If you need to use a specific openssl config file, you can reference it here. +# Normally this file is auto-detected from a file named openssl-1.0.cnf from the +# EASYRSA_PKI or EASYRSA dir (in that order.) NOTE that this file is Easy-RSA +# specific and you cannot just use a standard config file, so this is an +# advanced feature. + +set_var EASYRSA_SSL_CONF "$EASYRSA/openssl-1.0.cnf" + +# Default CN: +# This is best left alone. Interactively you will set this manually, and BATCH +# callers are expected to set this themselves. + +set_var EASYRSA_REQ_CN "{{ ansible_ssh_host }}" + +# Cryptographic digest to use. +# Do not change this default unless you understand the security implications. +# Valid choices include: md5, sha1, sha256, sha224, sha384, sha512 + +#set_var EASYRSA_DIGEST "sha256" + +# Batch mode. Leave this disabled unless you intend to call Easy-RSA explicitly +# in batch mode without any user input, confirmation on dangerous operations, +# or most output. Setting this to any non-blank string enables batch mode. + +set_var EASYRSA_BATCH "{{ ansible_ssh_host }}" diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 new file mode 100644 index 00000000..8bb61817 --- /dev/null +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -0,0 +1,34 @@ +config setup + uniqueids = never # allow multiple connections per user + charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" + +conn %default + dpdaction=clear + dpddelay=35s + dpdtimeout=300s + rekey=no + keyexchange=ikev2 + ike=aes128gcm16-sha2_256-prfsha256-ecp256! + esp=aes128gcm16-sha2_256-ecp256! + compress=yes + fragmentation=yes + + left=%any + leftauth=pubkey + leftid={{ ansible_ssh_host }} + leftcert={{ ansible_ssh_host }}.crt + leftsendcert=always + leftsubnet=0.0.0.0/0,::/0 + + right=%any + rightauth=pubkey + rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} +{% if service_dns is defined and service_dns == "N" %} + rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} +{% else %} + rightdns=172.16.0.1 +{% endif %} + + +conn ikev2-pubkey + auto=add diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/vpn/templates/ipsec.secrets.j2 new file mode 100644 index 00000000..cc208a59 --- /dev/null +++ b/roles/vpn/templates/ipsec.secrets.j2 @@ -0,0 +1,2 @@ +: ECDSA {{ ansible_ssh_host }}.key + diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 new file mode 100644 index 00000000..d1a235c6 --- /dev/null +++ b/roles/vpn/templates/mobileconfig.j2 @@ -0,0 +1,144 @@ + + + + + PayloadContent + + + IKEv2 + + AuthenticationMethod + Certificate + ChildSecurityAssociationParameters + + DiffieHellmanGroup + 19 + EncryptionAlgorithm + AES-128-GCM + IntegrityAlgorithm + SHA2-256 + LifeTimeInMinutes + 1440 + + DeadPeerDetectionRate + Medium + DisableMOBIKE + 0 + DisableRedirect + 0 + EnableCertificateRevocationCheck + 0 + EnablePFS + + IKESecurityAssociationParameters + + DiffieHellmanGroup + 19 + EncryptionAlgorithm + AES-128-GCM + IntegrityAlgorithm + SHA2-256 + LifeTimeInMinutes + 1440 + + LocalIdentifier + {{ item.0 }} + PayloadCertificateUUID + 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + CertificateType + ECDSA256 + ServerCertificateIssuerCommonName + {{ ansible_ssh_host }} + RemoteAddress + {{ ansible_ssh_host }} + RemoteIdentifier + {{ ansible_ssh_host }} + UseConfigurationAttributeInternalIPSubnet + 0 + + IPv4 + + OverridePrimary + 1 + + PayloadDescription + Configures VPN settings + PayloadDisplayName + VPN + PayloadIdentifier + com.apple.vpn.managed.D247A30B-6023-4C8E-B3E3-FF1910A65E53 + PayloadType + com.apple.vpn.managed + PayloadUUID + D247A30B-6023-4C8E-B3E3-FF1910A65E53 + PayloadVersion + 1 + Proxies + + HTTPEnable + 0 + HTTPSEnable + 0 + + UserDefinedName + {{ ansible_ssh_host }} IKEv2 + VPNType + IKEv2 + + + Password + {{ easyrsa_p12_export_password }} + PayloadCertificateFileName + {{ item.0 }}.p12 + PayloadContent + + {{ item.1.stdout }} + + PayloadDescription + Adds a PKCS#12-formatted certificate + PayloadDisplayName + {{ item.0 }}.p12 + PayloadIdentifier + com.apple.security.pkcs12.1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + PayloadType + com.apple.security.pkcs12 + PayloadUUID + 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + PayloadVersion + 1 + + + PayloadCertificateFileName + ca.crt + PayloadContent + + {{ PayloadContentCA.stdout }} + + PayloadDescription + Adds a CA root certificate + PayloadDisplayName + {{ ansible_ssh_host }} + PayloadIdentifier + com.apple.security.root.32EA3AAA-D19E-43EF-B357-608218745A38 + PayloadType + com.apple.security.root + PayloadUUID + 32EA3AAA-D19E-43EF-B357-608218745A38 + PayloadVersion + 1 + + + PayloadDisplayName + {{ ansible_ssh_host }} IKEv2 + PayloadIdentifier + donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BE + PayloadRemovalDisallowed + + PayloadType + Configuration + PayloadUUID + 743B04A8-5725-45A2-B1BB-836F8C16DB0A + PayloadVersion + 1 + + From e8f6864527d1f5692fd313d66f589bdb9b78de23 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 20:07:39 +0300 Subject: [PATCH 027/769] README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d74e9e3..cc635c40 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ To install the dependencies on OS X or Linux: ``` sudo easy_install pip -sudo pip install ansible dopy==0.3.5 boto +sudo pip install ansible dopy==0.3.5 boto apache-libcloud six ``` To install the dependencies for installing on an existing Ubuntu 16.04 system: @@ -56,6 +56,7 @@ sudo apt-get update && sudo apt-get install ansible There are three available installation targets: * DigitalOcean * Amazon EC2 +* Google Cloud Engine * Local servers Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. From 418968f7c338393d574b6017562d2e1d3c40a919 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 20:13:42 +0300 Subject: [PATCH 028/769] deploy.yml unnecessary --- algo | 2 +- deploy.yml | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 deploy.yml diff --git a/algo b/algo index 01523c0f..c62bf664 100755 --- a/algo +++ b/algo @@ -21,7 +21,7 @@ Enter the number of your desired provider *) exit 1 ;; esac - ansible-playbook deploy.yml -e "provider=${CLOUD}" + ansible-playbook "${CLOUD}.yml" } user_management () { diff --git a/deploy.yml b/deploy.yml deleted file mode 100644 index 5d680691..00000000 --- a/deploy.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -- include: "{{ provider }}.yml" - From 4d7d8c747a4d267466918a18035aad8833342438 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 14 Aug 2016 20:26:32 +0300 Subject: [PATCH 029/769] client cert password #45 --- algo | 17 ++++++++++++++--- config.cfg | 1 - 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/algo b/algo index c62bf664..8e1c48e2 100755 --- a/algo +++ b/algo @@ -1,5 +1,13 @@ #!/bin/sh +p12_export_password () { + echo -n " +Enter the password for p12 certificates (default: vpn): +: " + read -s P + P=${P:-vpn} +} + algo_provisioning () { echo -n " What provider would you like to use? @@ -20,12 +28,15 @@ Enter the number of your desired provider 0) CLOUD="non-cloud" ;; *) exit 1 ;; esac - - ansible-playbook "${CLOUD}.yml" + + p12_export_password + + ansible-playbook "${CLOUD}.yml" -e easyrsa_p12_export_password=${P} } user_management () { - ansible-playbook users.yml + p12_export_password + ansible-playbook users.yml -e easyrsa_p12_export_password=${P} } case "$1" in diff --git a/config.cfg b/config.cfg index c17bbf47..4daa91a0 100644 --- a/config.cfg +++ b/config.cfg @@ -3,7 +3,6 @@ easyrsa_dir: /opt/easy-rsa-ipsec easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 -easyrsa_p12_export_password: vpn # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False From 470f60a46f7d0599b09c3db77d7668b6fb3eb988 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 14 Aug 2016 20:42:30 -0400 Subject: [PATCH 030/769] Update README.md --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index cc635c40..932962e5 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,7 @@ sudo easy_install pip sudo pip install ansible dopy==0.3.5 boto apache-libcloud six ``` -To install the dependencies for installing on an existing Ubuntu 16.04 system: - -``` -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible -``` - -There are three available installation targets: +There are four available installation targets: * DigitalOcean * Amazon EC2 * Google Cloud Engine From f538ffe4e8c2f27fb17f56d58acfad2db2adcc64 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 15 Aug 2016 23:32:44 -0400 Subject: [PATCH 031/769] linting --- README.md | 9 +--- azure.yml | 22 ++++----- digitalocean.yml | 36 +++++++------- ec2.yml | 49 ++++++++++--------- google_cloud.yml | 23 ++++----- non-cloud.yml | 27 ++++++----- roles/common/handlers/main.yml | 5 +- roles/common/tasks/main.yml | 28 +++++------ roles/digitalocean/tasks/main.yml | 1 - roles/ec2/tasks/main.yml | 5 +- roles/features/handlers/main.yml | 16 +++---- roles/features/tasks/main.yml | 78 +++++++++++++++---------------- roles/google_cloud/tasks/main.yml | 12 ++--- roles/logging/tasks/main.yml | 5 +- roles/security/handlers/main.yml | 4 +- roles/security/tasks/main.yml | 36 +++++++------- roles/vpn/handlers/main.yml | 6 +-- roles/vpn/tasks/main.yml | 50 ++++++++++---------- users.yml | 29 ++++++------ 19 files changed, 206 insertions(+), 235 deletions(-) diff --git a/README.md b/README.md index 932962e5..def5464a 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere ## Features * Supports only IKEv2 -* Supports only AES GCM, SHA2 HMAC, and P-256 DH +* Supports only a single cipher suite w/ AES GCM, SHA2 HMAC, and P-256 DH * Generates mobileconfig profiles to auto-configure Apple devices * Provides helper scripts to add and remove users * Blocks ads with a local DNS resolver and HTTP proxy (optional) * Based on current versions of Ubuntu and StrongSwan +* Installs to DigitalOcean, Amazon EC2, Google Cloud Engine, or your own server ## Anti-features @@ -46,12 +47,6 @@ sudo easy_install pip sudo pip install ansible dopy==0.3.5 boto apache-libcloud six ``` -There are four available installation targets: -* DigitalOcean -* Amazon EC2 -* Google Cloud Engine -* Local servers - Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. Start the deploy and follow the instructions: diff --git a/azure.yml b/azure.yml index 5e35b77d..06098fb7 100644 --- a/azure.yml +++ b/azure.yml @@ -31,7 +31,7 @@ #- name: "ssh_public_key" #prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" - #private: no + #private: no #- name: "region" #prompt: > @@ -54,22 +54,22 @@ #Enter the number of your desired region: #default: "7" #private: no - + #- name: "azure_server_name" #prompt: "Name the vpn server:\n" #default: "algo.local" #private: no - + #- name: "dns_enabled" #prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" #default: "Y" #private: no - + #- name: "auditd_enabled" #prompt: "Do you want to use auditd ? (Y or N):\n" #default: "Y" - #private: no - + #private: no + roles: - azure @@ -79,21 +79,17 @@ become: true vars_files: - config.cfg - + pre_tasks: - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 roles: - common - security - features - vpn - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - - - + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/digitalocean.yml b/digitalocean.yml index ecd6262f..e82d3940 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -41,22 +41,22 @@ Enter the number of your desired region: default: "7" private: no - + - name: "do_server_name" prompt: "Name the vpn server:\n" default: "algo.local" private: no - + - name: "dns_enabled" prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" default: "Y" private: no - + - name: "auditd_enabled" prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" - private: no - + private: no + roles: - digitalocean @@ -66,13 +66,13 @@ become: true vars_files: - config.cfg - + pre_tasks: - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + - name: Enable IPv6 on the droplet uri: url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" @@ -83,7 +83,7 @@ status_code: 201 HEADER_Authorization: "Bearer {{ do_access_token }}" HEADER_Content-Type: "application/json" - + - name: Get Droplet networks uri: url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" @@ -97,30 +97,26 @@ template: src=roles/digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 with_items: "{{ droplet_info.json.droplet.networks.v6 }}" notify: - - reload eth0 - + - reload eth0 + - name: IPv6 included into the network config lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/20-ipv6.cfg' state=present notify: - reload eth0 - + - meta: flush_handlers - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" become: false - + roles: - common - security - features - vpn - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + handlers: - name: reload eth0 - shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' - - - - + shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' diff --git a/ec2.yml b/ec2.yml index 2e1bdfd7..0a11e48b 100644 --- a/ec2.yml +++ b/ec2.yml @@ -3,7 +3,7 @@ hosts: localhost gather_facts: False vars_files: - - config.cfg + - config.cfg vars: instance_type: t2.nano security_group: vpn-secgroup @@ -21,71 +21,70 @@ "11": "sa-east-1" vars_prompt: - + - name: "aws_access_key" prompt: "Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" private: yes - + - name: "aws_secret_key" prompt: "Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" - private: yes - + private: yes + - name: "region" prompt: > What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-west-1 US West (N. California) + 1. us-east-1 US East (N. Virginia) + 2. us-west-1 US West (N. California) 3. us-west-2 US West (Oregon) - 4. ap-south-1 Asia Pacific (Mumbai) - 5. ap-northeast-2 Asia Pacific (Seoul) - 6. ap-southeast-1 Asia Pacific (Singapore) - 7. ap-southeast-2 Asia Pacific (Sydney) - 8. ap-northeast-1 Asia Pacific (Tokyo) - 9. eu-central-1 EU (Frankfurt) - 10. eu-west-1 EU (Ireland) + 4. ap-south-1 Asia Pacific (Mumbai) + 5. ap-northeast-2 Asia Pacific (Seoul) + 6. ap-southeast-1 Asia Pacific (Singapore) + 7. ap-southeast-2 Asia Pacific (Sydney) + 8. ap-northeast-1 Asia Pacific (Tokyo) + 9. eu-central-1 EU (Frankfurt) + 10. eu-west-1 EU (Ireland) 11. sa-east-1 South America (São Paulo) default: "1" private: no - + - name: "aws_server_name" prompt: "Name the vpn server:\n" default: "algo.local" - private: no - + private: no + - name: "ssh_public_key" prompt: "Enter the local path to your SSH public key (ex: ~/.ssh/id_rsa.pub):\n" - private: no + private: no - name: "dns_enabled" prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" default: "Y" private: no - + - name: "auditd_enabled" prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no roles: - - ec2 - + - ec2 + - name: Post-provisioning tasks hosts: vpn-host gather_facts: false become: true vars_files: - config.cfg - + pre_tasks: - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - + roles: - common - security - features - vpn - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/google_cloud.yml b/google_cloud.yml index 504f82f9..d1cd9d4a 100644 --- a/google_cloud.yml +++ b/google_cloud.yml @@ -22,10 +22,10 @@ - name: "credentials_file" prompt: "Enter the local path to your credentials JSON file [ex: ~/gogle_cloud.json] (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts):\n" private: no - + - name: "ssh_public_key" prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" - private: no + private: no - name: "zone" prompt: > @@ -46,22 +46,22 @@ Please choose the number of your zone. Press enter for default (#8) zone. default: "8" private: no - + - name: "server_name" prompt: "Name the vpn server:\n" default: "algo" private: no - + - name: "dns_enabled" prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" default: "Y" private: no - + - name: "auditd_enabled" prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no - + roles: - google_cloud @@ -71,21 +71,16 @@ become: true vars_files: - config.cfg - + pre_tasks: - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 roles: - common - security - features - vpn - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - - - - + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/non-cloud.yml b/non-cloud.yml index 19a9c77a..d0f32f58 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -1,31 +1,31 @@ - hosts: localhost gather_facts: False vars_files: - - config.cfg + - config.cfg vars_prompt: - + - name: "server_ip" prompt: "Enter IP address of your server:\n" private: no - + - name: "server_user" prompt: "What user should we use to login on the server?:\n" default: "root" private: no - + - name: "dns_enabled" prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" default: "Y" private: no - + - name: "auditd_enabled" prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" - private: no - + private: no + tasks: - name: Add the server to the vpn-host group - add_host: + add_host: hostname: "{{ server_ip }}" groupname: vpn-host ansible_ssh_user: "{{ server_user }}" @@ -36,24 +36,23 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" become: false - + - name: Post-provisioning tasks hosts: vpn-host gather_facts: false become: true vars_files: - config.cfg - + pre_tasks: - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + roles: - common - security - features - vpn - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - + - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index d7a822d3..65e6972a 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -1,9 +1,8 @@ - name: restart rsyslog service: name=rsyslog state=restarted - + - name: restart ssh service: name=ssh state=restarted - + - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush - diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 078726ad..f258515f 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,7 +2,7 @@ - name: Gather Facts setup: - + - name: Install software updates apt: update_cache=yes upgrade=dist @@ -10,9 +10,9 @@ shell: > if [[ $(readlink -f /vmlinuz) != /boot/vmlinuz-$(uname -r) ]]; then echo "required"; else echo "no"; fi args: - executable: /bin/bash + executable: /bin/bash register: reboot_required - + - name: Reboot shell: sleep 2 && shutdown -r now "Ansible updates triggered" async: 1 @@ -23,30 +23,30 @@ - name: Wait for shutdown local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped timeout=120 when: reboot_required is defined and reboot_required.stdout == 'required' - become: false + become: false - name: Wait until SSH becomes ready... local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=120 when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - + become: false + # SSH fixes - + - name: SSH config lineinfile: dest="{{ item.file }}" regexp="{{ item.regexp }}" line="{{ item.line }}" state=present with_items: - { regexp: '^PasswordAuthentication.*', line: 'PasswordAuthentication no', file: '/etc/ssh/sshd_config' } - { regexp: '^PermitRootLogin.*', line: 'PermitRootLogin without-password', file: '/etc/ssh/sshd_config' } - { regexp: '^UseDNS.*', line: 'UseDNS no', file: '/etc/ssh/sshd_config' } - - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } + - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } - { regexp: '^MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com', file: '/etc/ssh/sshd_config' } - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384', file: '/etc/ssh/sshd_config' } notify: - - restart ssh - + - restart ssh + - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" - with_items: + with_items: - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } @@ -62,9 +62,9 @@ - sendmail - unattended-upgrades - iptables-persistent - + - name: Configure unattended-upgrades - template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=644 + template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 - name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=644 + template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 diff --git a/roles/digitalocean/tasks/main.yml b/roles/digitalocean/tasks/main.yml index 8f0c20f8..0fa41204 100644 --- a/roles/digitalocean/tasks/main.yml +++ b/roles/digitalocean/tasks/main.yml @@ -36,4 +36,3 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" - diff --git a/roles/ec2/tasks/main.yml b/roles/ec2/tasks/main.yml index 52a5fac8..75bd4f2e 100644 --- a/roles/ec2/tasks/main.yml +++ b/roles/ec2/tasks/main.yml @@ -64,7 +64,7 @@ register: ec2 - name: Add new instance to host group - add_host: + add_host: hostname: "{{ item.public_ip }}" groupname: vpn-host ansible_ssh_user: ubuntu @@ -76,5 +76,4 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ item.public_dns_name }} timeout=320" with_items: "{{ ec2.instances }}" - become: false - + become: false diff --git a/roles/features/handlers/main.yml b/roles/features/handlers/main.yml index 284064fc..c42aea8c 100644 --- a/roles/features/handlers/main.yml +++ b/roles/features/handlers/main.yml @@ -1,17 +1,17 @@ - name: restart privoxy service: name=privoxy state=restarted - + - name: restart dnsmasq - service: name=dnsmasq state=restarted - + service: name=dnsmasq state=restarted + - name: restart apparmor service: name=apparmor state=restarted - + - name: restart apache2 - service: name=apache2 state=restarted - + service: name=apache2 state=restarted + - name: save iptables command: service netfilter-persistent save - + - name: restart loopback - shell: ifdown lo:100 && ifup lo:100 + shell: ifdown lo:100 && ifup lo:100 diff --git a/roles/features/tasks/main.yml b/roles/features/tasks/main.yml index 8045981a..a1454e8b 100644 --- a/roles/features/tasks/main.yml +++ b/roles/features/tasks/main.yml @@ -1,19 +1,19 @@ - name: Gather Facts setup: - + - name: Loopback for services configured template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg notify: - restart loopback - + - name: Loopback included into the network config lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present notify: - restart loopback - + - meta: flush_handlers - -# Privoxy + +# Privoxy - name: Privoxy installed apt: name=privoxy state=latest @@ -22,36 +22,36 @@ template: src=privoxy_config.j2 dest=/etc/privoxy/config notify: - restart privoxy - + - name: Privoxy profile for apparmor configured - template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=600 + template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 notify: - restart privoxy - + - name: Enforce the privoxy AppArmor policy - shell: aa-enforce usr.sbin.privoxy - + shell: aa-enforce usr.sbin.privoxy + - name: Privoxy enabled and started service: name=privoxy state=started enabled=yes - + # PageSpeed - name: Apache installed apt: name=apache2 state=latest - + - name: PageSpeed installed for x86_64 apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb when: ansible_architecture == "x86_64" - + - name: PageSpeed installed for i386 apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb - when: ansible_architecture != "x86_64" - + when: ansible_architecture != "x86_64" + - name: PageSpeed configured template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf notify: - - restart apache2 - + - restart apache2 + - name: Modules enabled apache2_module: state=present name="{{ item }}" with_items: @@ -63,39 +63,39 @@ - rewrite notify: - restart apache2 - + - name: VirtualHost configured for the PageSpeed module template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf notify: - - restart apache2 - + - restart apache2 + - name: Apache ports configured template: src=ports.conf.j2 dest=/etc/apache2/ports.conf notify: - - restart apache2 - -# DNS + - restart apache2 + +# DNS - name: Dnsmasq installed - apt: name=dnsmasq state=latest - + apt: name=dnsmasq state=latest + - name: Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=600 + template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 notify: - restart dnsmasq - + - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq - + - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf notify: - restart dnsmasq - + - name: Adblock script created - template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=755 + template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 when: dns_enabled is defined and dns_enabled == "Y" - + - name: Adblock script added to cron cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" when: dns_enabled is defined and dns_enabled == "Y" @@ -104,9 +104,9 @@ shell: > /opt/adblock.sh when: dns_enabled is defined and dns_enabled == "Y" - + - name: Forward all DNS requests to the local resolver - iptables: + iptables: table: nat chain: PREROUTING protocol: udp @@ -117,9 +117,9 @@ notify: - save iptables when: dns_enabled is defined and dns_enabled == "Y" - + - name: Forward all DNS requests to the local resolver - iptables: + iptables: table: nat chain: PREROUTING protocol: udp @@ -130,12 +130,12 @@ ip_version: ipv6 notify: - save iptables - when: dns_enabled is defined and dns_enabled == "Y" - + when: dns_enabled is defined and dns_enabled == "Y" + - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes when: dns_enabled is defined and dns_enabled == "Y" - + - name: Dnsmasq disabled and stopped service: name=dnsmasq state=stopped enabled=no - when: dns_enabled is defined and dns_enabled != "Y" + when: dns_enabled is defined and dns_enabled != "Y" diff --git a/roles/google_cloud/tasks/main.yml b/roles/google_cloud/tasks/main.yml index dbe5c6c6..b8957b14 100644 --- a/roles/google_cloud/tasks/main.yml +++ b/roles/google_cloud/tasks/main.yml @@ -13,7 +13,7 @@ project_id: "{{ credentials_file_lookup.project_id }}" metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' register: google_vm - + - name: Add the droplet to an inventory group add_host: name: "{{ google_vm.instance_data[0].public_ip}}" @@ -22,7 +22,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" - + - name: Firewall configured local_action: module: gce_net @@ -31,13 +31,9 @@ allowed: "udp:500,4500;tcp:22" state: "present" src_range: 0.0.0.0/0 - service_account_email: "{{ credentials_file_lookup.client_email }}" + service_account_email: "{{ credentials_file_lookup.client_email }}" credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + project_id: "{{ credentials_file_lookup.project_id }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ google_vm.instance_data[0].public_ip }} timeout=320" - - - - diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml index e6a88854..fdda9376 100644 --- a/roles/logging/tasks/main.yml +++ b/roles/logging/tasks/main.yml @@ -5,12 +5,11 @@ template: src=audit.rules.j2 dest=/etc/audit/audit.rules notify: - restart auditd - + - name: Auditd configured template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf notify: - restart auditd - + - name: Enable services service: name=auditd enabled=yes - diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index dd2210b6..da5c0922 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart rsyslog service: name=rsyslog state=restarted - + - name: flush routing cache - shell: echo 1 > /proc/sys/net/ipv4/route/flush + shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index e7fa93e8..071f6ff1 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -15,38 +15,38 @@ file: path='{{ item.stat.path }}' mode='go-w' recurse=yes when: item.stat.isdir with_items: "{{ minimize_access_directories.results }}" - no_log: True + no_log: True - name: Change shadow ownership to root and mode to 0600 file: dest='/etc/shadow' owner=root group=root mode=0600 - name: change su-binary to only be accessible to user and group root file: dest='/bin/su' owner=root group=root mode=0750 - + - name: Collect Use of privileged commands - shell: > + shell: > /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' args: executable: /bin/bash - register: privileged_programs - + register: privileged_programs + # Rsyslog - + - name: Rsyslog configured template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf notify: - - restart rsyslog + - restart rsyslog - name: Rsyslog CIS configured template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 notify: - - restart rsyslog - + - restart rsyslog + - name: Enable services service: name=rsyslog enabled=yes # Core dumps - + - name: Restrict core dumps (with PAM) lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present @@ -62,13 +62,13 @@ - net.ipv4.conf.default.accept_source_route notify: - flush routing cache - + - name: Disable ICMP Redirect Acceptance sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present with_items: - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects - + - net.ipv4.conf.default.accept_redirects + - name: Disable Secure ICMP Redirect Acceptance sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present with_items: @@ -76,12 +76,12 @@ - net.ipv4.conf.default.secure_redirects notify: - flush routing cache - + - name: Enable Bad Error Message Protection sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present notify: - flush routing cache - + - name: Enable RFC-recommended Source Route Validation sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present with_items: @@ -89,12 +89,12 @@ - net.ipv4.conf.default.rp_filter notify: - flush routing cache - + - name: Enable packet forwarding for IPv4 sysctl: name=net.ipv4.ip_forward value=1 - + - name: Enable packet forwarding for IPv6 - sysctl: name=net.ipv6.conf.all.forwarding value=1 + sysctl: name=net.ipv6.conf.all.forwarding value=1 - name: Do not send ICMP redirects (we are not a router) sysctl: name=net.ipv4.conf.all.send_redirects value=0 diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index d070b51a..0885344e 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -3,10 +3,10 @@ - name: restart apparmor service: name=apparmor state=restarted - + - name: save iptables - command: service netfilter-persistent save - + command: service netfilter-persistent save + - name: congrats debug: msg: diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index c1bf4f8f..8bbbcb50 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -9,7 +9,7 @@ - /usr/lib/ipsec/stroke notify: - restart apparmor - + - name: Enable services service: name={{ item }} enabled=yes with_items: @@ -21,32 +21,32 @@ iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE notify: - save iptables - + - name: Configure ip6tables so IPSec traffic can traverse the tunnel iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE notify: - - save iptables - + - save iptables + - name: Setup the ipsec.conf file from our template - template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=644 + template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 notify: - - restart strongswan + - restart strongswan - name: Setup the ipsec.secrets file - template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=root group=root mode=600 + template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=root group=root mode=0600 notify: - - restart strongswan - + - restart strongswan + - name: Fetch easy-rsa-ipsec from git - git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git dest="{{ easyrsa_dir }}" + git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git version=ed4de10d7ce0726357fb1bb4729f8eb440c06e2b dest="{{ easyrsa_dir }}" - name: Setup the vars file from our template template: src=easy-rsa.vars.j2 dest={{ easyrsa_dir }}/easyrsa3/vars - + - name: Ensure the pki directory is not exist file: dest={{ easyrsa_dir }}/easyrsa3/pki state=absent when: easyrsa_reinit_existent == True - + - name: Build the pki enviroments shell: > ./easyrsa init-pki && @@ -55,7 +55,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' -- name: Build the CA pair +- name: Build the CA pair shell: > ./easyrsa build-ca nopass && touch {{ easyrsa_dir }}/easyrsa3/pki/ca_initialized @@ -63,8 +63,8 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/ca_initialized' notify: - - restart strongswan - + - restart strongswan + - name: Build the server pair shell: > ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ ansible_ssh_host }}' build-server-full {{ ansible_ssh_host }} nopass&& @@ -73,7 +73,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' notify: - - restart strongswan + - restart strongswan - name: Build the client's pair shell: > @@ -84,7 +84,7 @@ creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' with_items: "{{ users }}" -- name: Build the client's p12 +- name: Build the client's p12 shell: > openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' @@ -92,7 +92,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' with_items: "{{ users }}" - + - name: Copy the CA cert to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=root group=root mode=0600 notify: @@ -101,12 +101,12 @@ - name: Copy the server cert to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ ansible_ssh_host }}.crt' dest=/etc/ipsec.d/certs/{{ ansible_ssh_host }}.crt owner=root group=root mode=0600 notify: - - restart strongswan - + - restart strongswan + - name: Copy the server key to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ ansible_ssh_host }}.key' dest=/etc/ipsec.d/private/{{ ansible_ssh_host }}.key owner=root group=root mode=0600 notify: - - restart strongswan + - restart strongswan - name: Register p12 PayloadContent shell: > @@ -118,14 +118,14 @@ shell: > cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 register: PayloadContentCA - + - name: Build the mobileconfigs template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 with_together: - "{{ users }}" - "{{ PayloadContent.results }}" - no_log: True - + no_log: True + - name: Fetch users P12 fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" @@ -133,7 +133,7 @@ - name: Fetch users mobileconfig fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ server_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" - + - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes notify: diff --git a/users.yml b/users.yml index e2060a4f..a7489c0e 100644 --- a/users.yml +++ b/users.yml @@ -3,21 +3,21 @@ - hosts: localhost gather_facts: False vars_files: - - config.cfg + - config.cfg vars_prompt: - + - name: "server_ip" prompt: "\nEnter IP address of your server:\n" private: no - + - name: "server_user" prompt: "What user should we use to login on the server?:\n" default: "root" private: no - + tasks: - name: Add the server to the vpn-host group - add_host: + add_host: hostname: "{{ server_ip }}" groupname: vpn-host ansible_ssh_user: "{{ server_user }}" @@ -25,15 +25,15 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" - become: false + become: false - name: User management hosts: vpn-host gather_facts: false become: true vars_files: - - config.cfg - + - config.cfg + tasks: - name: Build the client's pair shell: > @@ -52,14 +52,14 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' with_items: "{{ users }}" - + - name: Get active users shell: > grep ^V pki/index.txt | grep -v "{{ server_name }}" | awk '{print $5}' | sed 's/\/CN=//g' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' register: valid_certs - + - name: Revoke non-existing users shell: > ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && @@ -69,7 +69,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' when: item not in users with_items: "{{ valid_certs.stdout_lines }}" - + - name: Register p12 PayloadContent shell: > cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 @@ -80,14 +80,14 @@ shell: > cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 register: PayloadContentCA - + - name: Build the mobileconfigs template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 with_together: - "{{ users }}" - "{{ PayloadContent.results }}" - no_log: True - + no_log: True + - name: Fetch users P12 fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" @@ -98,4 +98,3 @@ - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes - From 0fd0de17d44830d637cd3911cf06d2b5e7ad583e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 16 Aug 2016 00:00:26 -0400 Subject: [PATCH 032/769] rename the cloud roles --- algo | 15 ++++++--------- azure.yml | 2 +- digitalocean.yml | 2 +- ec2.yml | 2 +- google_cloud.yml => gce.yml | 2 +- roles/{azure => cloud-azure}/handlers/main.yml | 0 roles/{azure => cloud-azure}/tasks/main.yml | 0 .../handlers/main.yml | 0 .../tasks/main.yml | 0 .../templates/20-ipv6.cfg.j2 | 0 roles/{ec2 => cloud-ec2}/handlers/main.yml | 0 roles/{ec2 => cloud-ec2}/tasks/main.yml | 0 .../{google_cloud => cloud-gce}/handlers/main.yml | 0 roles/{google_cloud => cloud-gce}/tasks/main.yml | 0 14 files changed, 10 insertions(+), 13 deletions(-) rename google_cloud.yml => gce.yml (99%) rename roles/{azure => cloud-azure}/handlers/main.yml (100%) rename roles/{azure => cloud-azure}/tasks/main.yml (100%) rename roles/{digitalocean => cloud-digitalocean}/handlers/main.yml (100%) rename roles/{digitalocean => cloud-digitalocean}/tasks/main.yml (100%) rename roles/{digitalocean => cloud-digitalocean}/templates/20-ipv6.cfg.j2 (100%) rename roles/{ec2 => cloud-ec2}/handlers/main.yml (100%) rename roles/{ec2 => cloud-ec2}/tasks/main.yml (100%) rename roles/{google_cloud => cloud-gce}/handlers/main.yml (100%) rename roles/{google_cloud => cloud-gce}/tasks/main.yml (100%) diff --git a/algo b/algo index 8e1c48e2..326b3670 100755 --- a/algo +++ b/algo @@ -13,8 +13,8 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 - 3. Google-cloud - 0. Local installation (non-cloud or a server already deployed) + 3. Google Cloud Engine + 4. Remote installation (install to existing Ubuntu server) Enter the number of your desired provider : " @@ -22,12 +22,12 @@ Enter the number of your desired provider read N case "$N" in - 1) CLOUD="digitalocean" ;; + 1) CLOUD="digitalocean" ;; 2) CLOUD="ec2" ;; - 3) CLOUD="google_cloud" ;; - 0) CLOUD="non-cloud" ;; + 3) CLOUD="gce" ;; + 4) CLOUD="non-cloud" ;; *) exit 1 ;; - esac + esac p12_export_password @@ -43,6 +43,3 @@ case "$1" in update-users) user_management ;; *) algo_provisioning ;; esac - - - diff --git a/azure.yml b/azure.yml index 06098fb7..11be810f 100644 --- a/azure.yml +++ b/azure.yml @@ -71,7 +71,7 @@ #private: no roles: - - azure + - cloud-azure - name: Post-provisioning tasks hosts: vpn-host diff --git a/digitalocean.yml b/digitalocean.yml index e82d3940..f59dde84 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -58,7 +58,7 @@ private: no roles: - - digitalocean + - cloud-digitalocean - name: Post-provisioning tasks hosts: vpn-host diff --git a/ec2.yml b/ec2.yml index 0a11e48b..11f5f7e0 100644 --- a/ec2.yml +++ b/ec2.yml @@ -67,7 +67,7 @@ private: no roles: - - ec2 + - cloud-ec2 - name: Post-provisioning tasks hosts: vpn-host diff --git a/google_cloud.yml b/gce.yml similarity index 99% rename from google_cloud.yml rename to gce.yml index d1cd9d4a..9bd577ce 100644 --- a/google_cloud.yml +++ b/gce.yml @@ -63,7 +63,7 @@ private: no roles: - - google_cloud + - cloud-gce - name: Post-provisioning tasks hosts: vpn-host diff --git a/roles/azure/handlers/main.yml b/roles/cloud-azure/handlers/main.yml similarity index 100% rename from roles/azure/handlers/main.yml rename to roles/cloud-azure/handlers/main.yml diff --git a/roles/azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml similarity index 100% rename from roles/azure/tasks/main.yml rename to roles/cloud-azure/tasks/main.yml diff --git a/roles/digitalocean/handlers/main.yml b/roles/cloud-digitalocean/handlers/main.yml similarity index 100% rename from roles/digitalocean/handlers/main.yml rename to roles/cloud-digitalocean/handlers/main.yml diff --git a/roles/digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml similarity index 100% rename from roles/digitalocean/tasks/main.yml rename to roles/cloud-digitalocean/tasks/main.yml diff --git a/roles/digitalocean/templates/20-ipv6.cfg.j2 b/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 similarity index 100% rename from roles/digitalocean/templates/20-ipv6.cfg.j2 rename to roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 diff --git a/roles/ec2/handlers/main.yml b/roles/cloud-ec2/handlers/main.yml similarity index 100% rename from roles/ec2/handlers/main.yml rename to roles/cloud-ec2/handlers/main.yml diff --git a/roles/ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml similarity index 100% rename from roles/ec2/tasks/main.yml rename to roles/cloud-ec2/tasks/main.yml diff --git a/roles/google_cloud/handlers/main.yml b/roles/cloud-gce/handlers/main.yml similarity index 100% rename from roles/google_cloud/handlers/main.yml rename to roles/cloud-gce/handlers/main.yml diff --git a/roles/google_cloud/tasks/main.yml b/roles/cloud-gce/tasks/main.yml similarity index 100% rename from roles/google_cloud/tasks/main.yml rename to roles/cloud-gce/tasks/main.yml From 52855c9e3f418b985a07d92684368721ecb0a6c6 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 16 Aug 2016 00:03:26 -0400 Subject: [PATCH 033/769] Use the right language for GCE --- algo | 2 +- roles/cloud-gce/tasks/main.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/algo b/algo index 326b3670..769c6b37 100755 --- a/algo +++ b/algo @@ -13,7 +13,7 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 - 3. Google Cloud Engine + 3. Google Compute Engine 4. Remote installation (install to existing Ubuntu server) Enter the number of your desired provider diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index b8957b14..62f55402 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -2,7 +2,7 @@ credentials_file_lookup: "{{ lookup('file', '{{ credentials_file }}') }}" ssh_public_key_lookup: "{{ lookup('file', '{{ ssh_public_key }}') }}" -- name: "Creating a droplet..." +- name: "Creating a new instance..." gce: instance_names: "{{ server_name }}" zone: "{{ zones[zone] }}" @@ -14,7 +14,7 @@ metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' register: google_vm -- name: Add the droplet to an inventory group +- name: Add the instance to an inventory group add_host: name: "{{ google_vm.instance_data[0].public_ip}}" groups: vpn-host @@ -35,5 +35,5 @@ credentials_file: "{{ credentials_file }}" project_id: "{{ credentials_file_lookup.project_id }}" -- name: Wait for SSH to become available +- name: Waiting for SSH to become available local_action: "wait_for port=22 host={{ google_vm.instance_data[0].public_ip }} timeout=320" From 949370ea985be5104966c0bcf45169ed932198b5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 16 Aug 2016 09:39:39 -0400 Subject: [PATCH 034/769] oops --- digitalocean.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/digitalocean.yml b/digitalocean.yml index f59dde84..1e694bc6 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -94,7 +94,7 @@ register: droplet_info - name: IPv6 configured - template: src=roles/digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 + template: src=roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 with_items: "{{ droplet_info.json.droplet.networks.v6 }}" notify: - reload eth0 From 2a8c1adb76bdc93b47c573a04ea0762c63812fd4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 16 Aug 2016 23:31:20 -0400 Subject: [PATCH 035/769] Update main.yml --- roles/common/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index f258515f..3bdc3135 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -40,7 +40,7 @@ - { regexp: '^UseDNS.*', line: 'UseDNS no', file: '/etc/ssh/sshd_config' } - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } - { regexp: '^MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com', file: '/etc/ssh/sshd_config' } - - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384', file: '/etc/ssh/sshd_config' } + - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256', file: '/etc/ssh/sshd_config' } notify: - restart ssh From 95c43e221106940f7b261b2d4c82d22d875b358d Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Wed, 17 Aug 2016 23:26:17 +0300 Subject: [PATCH 036/769] Split the features role in two #49 --- azure.yml | 3 +- digitalocean.yml | 3 +- ec2.yml | 3 +- gce.yml | 3 +- non-cloud.yml | 3 +- roles/common/handlers/main.yml | 3 + roles/common/tasks/main.yml | 10 + roles/features/handlers/main.yml | 17 - roles/features/tasks/main.yml | 141 -- roles/features/templates/000-default.conf.j2 | 11 - .../templates/10-loopback-services.cfg.j2 | 9 - roles/features/templates/adblock.sh | 50 - roles/features/templates/dnsmasq.conf.j2 | 669 ------ roles/features/templates/pagespeed.conf.j2 | 369 --- roles/features/templates/ports.conf.j2 | 13 - roles/features/templates/privoxy_config.j2 | 2107 ----------------- roles/features/templates/usr.sbin.dnsmasq.j2 | 68 - roles/features/templates/usr.sbin.privoxy.j2 | 15 - 18 files changed, 23 insertions(+), 3474 deletions(-) delete mode 100644 roles/features/handlers/main.yml delete mode 100644 roles/features/tasks/main.yml delete mode 100644 roles/features/templates/000-default.conf.j2 delete mode 100644 roles/features/templates/10-loopback-services.cfg.j2 delete mode 100644 roles/features/templates/adblock.sh delete mode 100644 roles/features/templates/dnsmasq.conf.j2 delete mode 100644 roles/features/templates/pagespeed.conf.j2 delete mode 100644 roles/features/templates/ports.conf.j2 delete mode 100644 roles/features/templates/privoxy_config.j2 delete mode 100644 roles/features/templates/usr.sbin.dnsmasq.j2 delete mode 100644 roles/features/templates/usr.sbin.privoxy.j2 diff --git a/azure.yml b/azure.yml index 11be810f..ec15d2cc 100644 --- a/azure.yml +++ b/azure.yml @@ -89,7 +89,8 @@ roles: - common - security - - features + - proxy - vpn + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/digitalocean.yml b/digitalocean.yml index 1e694bc6..0954a7bd 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -113,8 +113,9 @@ roles: - common - security - - features + - proxy - vpn + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } handlers: diff --git a/ec2.yml b/ec2.yml index 11f5f7e0..5339a2dd 100644 --- a/ec2.yml +++ b/ec2.yml @@ -85,6 +85,7 @@ roles: - common - security - - features + - proxy - vpn + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/gce.yml b/gce.yml index 9bd577ce..093bfab2 100644 --- a/gce.yml +++ b/gce.yml @@ -81,6 +81,7 @@ roles: - common - security - - features + - proxy - vpn + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/non-cloud.yml b/non-cloud.yml index d0f32f58..b53ece7d 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -53,6 +53,7 @@ roles: - common - security - - features + - proxy - vpn + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 65e6972a..6e249d75 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -6,3 +6,6 @@ - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush + +- name: restart loopback + shell: ifdown lo:100 && ifup lo:100 diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 3bdc3135..afdc91fb 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -68,3 +68,13 @@ - name: Periodic upgrades configured template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 + +- name: Loopback for services configured + template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg + notify: + - restart loopback + +- name: Loopback included into the network config + lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present + notify: + - restart loopback diff --git a/roles/features/handlers/main.yml b/roles/features/handlers/main.yml deleted file mode 100644 index c42aea8c..00000000 --- a/roles/features/handlers/main.yml +++ /dev/null @@ -1,17 +0,0 @@ -- name: restart privoxy - service: name=privoxy state=restarted - -- name: restart dnsmasq - service: name=dnsmasq state=restarted - -- name: restart apparmor - service: name=apparmor state=restarted - -- name: restart apache2 - service: name=apache2 state=restarted - -- name: save iptables - command: service netfilter-persistent save - -- name: restart loopback - shell: ifdown lo:100 && ifup lo:100 diff --git a/roles/features/tasks/main.yml b/roles/features/tasks/main.yml deleted file mode 100644 index a1454e8b..00000000 --- a/roles/features/tasks/main.yml +++ /dev/null @@ -1,141 +0,0 @@ -- name: Gather Facts - setup: - -- name: Loopback for services configured - template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg - notify: - - restart loopback - -- name: Loopback included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present - notify: - - restart loopback - -- meta: flush_handlers - -# Privoxy - -- name: Privoxy installed - apt: name=privoxy state=latest - -- name: Privoxy configured - template: src=privoxy_config.j2 dest=/etc/privoxy/config - notify: - - restart privoxy - -- name: Privoxy profile for apparmor configured - template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 - notify: - - restart privoxy - -- name: Enforce the privoxy AppArmor policy - shell: aa-enforce usr.sbin.privoxy - -- name: Privoxy enabled and started - service: name=privoxy state=started enabled=yes - -# PageSpeed - -- name: Apache installed - apt: name=apache2 state=latest - -- name: PageSpeed installed for x86_64 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb - when: ansible_architecture == "x86_64" - -- name: PageSpeed installed for i386 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb - when: ansible_architecture != "x86_64" - -- name: PageSpeed configured - template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf - notify: - - restart apache2 - -- name: Modules enabled - apache2_module: state=present name="{{ item }}" - with_items: - - proxy_http - - pagespeed - - cache - - proxy_connect - - proxy_html - - rewrite - notify: - - restart apache2 - -- name: VirtualHost configured for the PageSpeed module - template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf - notify: - - restart apache2 - -- name: Apache ports configured - template: src=ports.conf.j2 dest=/etc/apache2/ports.conf - notify: - - restart apache2 - -# DNS - -- name: Dnsmasq installed - apt: name=dnsmasq state=latest - -- name: Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 - notify: - - restart dnsmasq - -- name: Enforce the dnsmasq AppArmor policy - shell: aa-enforce usr.sbin.dnsmasq - -- name: Dnsmasq configured - template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf - notify: - - restart dnsmasq - -- name: Adblock script created - template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Adblock script added to cron - cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Update adblock hosts - shell: > - /opt/adblock.sh - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network }}" - jump: DNAT - to_destination: 172.16.0.1:53 - notify: - - save iptables - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network_ipv6 }}" - jump: DNAT - to_destination: fcaa::1:53 - ip_version: ipv6 - notify: - - save iptables - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Dnsmasq enabled and started - service: name=dnsmasq state=started enabled=yes - when: dns_enabled is defined and dns_enabled == "Y" - -- name: Dnsmasq disabled and stopped - service: name=dnsmasq state=stopped enabled=no - when: dns_enabled is defined and dns_enabled != "Y" diff --git a/roles/features/templates/000-default.conf.j2 b/roles/features/templates/000-default.conf.j2 deleted file mode 100644 index 7aa917b7..00000000 --- a/roles/features/templates/000-default.conf.j2 +++ /dev/null @@ -1,11 +0,0 @@ - - - Order deny,allow - Allow from all - - RewriteEngine On - RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] - ProxyPass / http://$1 - ProxyPassReverse / http://$1 - ProxyPreserveHost On - diff --git a/roles/features/templates/10-loopback-services.cfg.j2 b/roles/features/templates/10-loopback-services.cfg.j2 deleted file mode 100644 index c5c47e47..00000000 --- a/roles/features/templates/10-loopback-services.cfg.j2 +++ /dev/null @@ -1,9 +0,0 @@ -auto lo:100 -iface lo:100 inet static - address 172.16.0.1 - netmask 255.255.255.255 - -iface lo:100 inet6 static - address FCAA::1 - netmask 64 - autoconf 0 diff --git a/roles/features/templates/adblock.sh b/roles/features/templates/adblock.sh deleted file mode 100644 index a6a88581..00000000 --- a/roles/features/templates/adblock.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -#Block ads, malware, etc. - -# Redirect endpoint -ENDPOINT_IP4="0.0.0.0" -ENDPOINT_IP6="::" -IPV6="Y" - -#Delete the old block.hosts to make room for the updates -rm -f /etc/block.hosts - -echo 'Downloading hosts lists...' -#Download and process the files needed to make the lists (enable/add more, if you want) -wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > /tmp/block.build.list -wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list - -#Add black list, if non-empty -if [ -s "/etc/black.list" ] -then - echo 'Adding blacklist...' - awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /etc/black.list >> /tmp/block.build.list -fi - -#Sort the download/black lists -awk '{sub(/\r$/,"");print $1,$2}' /tmp/block.build.list|sort -u > /tmp/block.build.before - -#Filter (if applicable) -if [ -s "/etc/white.list" ] -then - #Filter the blacklist, supressing whitelist matches - # This is relatively slow =-( - echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /etc/block.hosts -else - cat /tmp/block.build.before > /etc/block.hosts -fi - -if [ "$IPV6" = "Y" ] -then - safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') - safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') - echo 'Adding ipv6 support...' - sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /etc/block.hosts -fi - -service dnsmasq restart - -exit 0 diff --git a/roles/features/templates/dnsmasq.conf.j2 b/roles/features/templates/dnsmasq.conf.j2 deleted file mode 100644 index d28cfac3..00000000 --- a/roles/features/templates/dnsmasq.conf.j2 +++ /dev/null @@ -1,669 +0,0 @@ -# Configuration file for dnsmasq. -# -# Format is one option per line, legal options are the same -# as the long options legal on the command line. See -# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. - -# Listen on this specific port instead of the standard DNS port -# (53). Setting this to zero completely disables DNS function, -# leaving only DHCP and/or TFTP. -#port=5353 - -# The following two options make you a better netizen, since they -# tell dnsmasq to filter out queries which the public DNS cannot -# answer, and which load the servers (especially the root servers) -# unnecessarily. If you have a dial-on-demand link they also stop -# these requests from bringing up the link unnecessarily. - -# Never forward plain names (without a dot or domain part) -#domain-needed -# Never forward addresses in the non-routed address spaces. -#bogus-priv - -# Uncomment these to enable DNSSEC validation and caching: -# (Requires dnsmasq to be built with DNSSEC option.) -#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf -#dnssec - -# Replies which are not DNSSEC signed may be legitimate, because the domain -# is unsigned, or may be forgeries. Setting this option tells dnsmasq to -# check that an unsigned reply is OK, by finding a secure proof that a DS -# record somewhere between the root and the domain does not exist. -# The cost of setting this is that even queries in unsigned domains will need -# one or more extra DNS queries to verify. -#dnssec-check-unsigned - -# Uncomment this to filter useless windows-originated DNS requests -# which can trigger dial-on-demand links needlessly. -# Note that (amongst other things) this blocks all SRV requests, -# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk. -# This option only affects forwarding, SRV records originating for -# dnsmasq (via srv-host= lines) are not suppressed by it. -#filterwin2k - -# Change this line if you want dns to get its upstream servers from -# somewhere other that /etc/resolv.conf -#resolv-file= - -# By default, dnsmasq will send queries to any of the upstream -# servers it knows about and tries to favour servers to are known -# to be up. Uncommenting this forces dnsmasq to try each query -# with each server strictly in the order they appear in -# /etc/resolv.conf -#strict-order - -# If you don't want dnsmasq to read /etc/resolv.conf or any other -# file, getting its servers from this file instead (see below), then -# uncomment this. -#no-resolv - -# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv -# files for changes and re-read them then uncomment this. -#no-poll - -# Add other name servers here, with domain specs if they are for -# non-public domains. -#server=/localnet/192.168.0.1 - -# Example of routing PTR queries to nameservers: this will send all -# address->name queries for 192.168.3/24 to nameserver 10.1.2.3 -#server=/3.168.192.in-addr.arpa/10.1.2.3 - -# Add local-only domains here, queries in these domains are answered -# from /etc/hosts or DHCP only. -#local=/localnet/ - -# Add domains which you want to force to an IP address here. -# The example below send any host in double-click.net to a local -# web-server. -#address=/double-click.net/127.0.0.1 - -# --address (and --server) work with IPv6 addresses too. -#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 - -# Add the IPs of all queries to yahoo.com, google.com, and their -# subdomains to the vpn and search ipsets: -#ipset=/yahoo.com/google.com/vpn,search - -# You can control how dnsmasq talks to a server: this forces -# queries to 10.1.2.3 to be routed via eth1 -# server=10.1.2.3@eth1 -server=8.8.8.8 -server=8.8.4.4 - -# and this sets the source (ie local) address used to talk to -# 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that -# IP on the machine, obviously). -# server=10.1.2.3@192.168.1.1#55 - -# If you want dnsmasq to change uid and gid to something other -# than the default, edit the following lines. -user=nobody -group=nogroup - -# If you want dnsmasq to listen for DHCP and DNS requests only on -# specified interfaces (and the loopback) give the name of the -# interface (eg eth0) here. -# Repeat the line for more than one interface. -#interface=lo -# Or you can specify which interface _not_ to listen on -#except-interface= -# Or which to listen on by address (remember to include 127.0.0.1 if -# you use this.) -listen-address=172.16.0.1,127.0.0.1,FCAA::1 -# If you want dnsmasq to provide only DNS service on an interface, -# configure it as shown above, and then use the following line to -# disable DHCP and TFTP on it. -#no-dhcp-interface= - -# On systems which support it, dnsmasq binds the wildcard address, -# even when it is listening on only some interfaces. It then discards -# requests that it shouldn't reply to. This has the advantage of -# working even when interfaces come and go and change address. If you -# want dnsmasq to really bind only the interfaces it is listening on, -# uncomment this option. About the only time you may need this is when -# running another nameserver on the same machine. -bind-interfaces - -# If you don't want dnsmasq to read /etc/hosts, uncomment the -# following line. -#no-hosts -# or if you want it to read another file, as well as /etc/hosts, use -# this. -addn-hosts=/etc/block.hosts - -# Set this (and domain: see below) if you want to have a domain -# automatically added to simple names in a hosts-file. -#expand-hosts - -# Set the domain for dnsmasq. this is optional, but if it is set, it -# does the following things. -# 1) Allows DHCP hosts to have fully qualified domain names, as long -# as the domain part matches this setting. -# 2) Sets the "domain" DHCP option thereby potentially setting the -# domain of all systems configured by DHCP -# 3) Provides the domain part for "expand-hosts" -#domain=thekelleys.org.uk - -# Set a different domain for a particular subnet -#domain=wireless.thekelleys.org.uk,192.168.2.0/24 - -# Same idea, but range rather then subnet -#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200 - -# Uncomment this to enable the integrated DHCP server, you need -# to supply the range of addresses available for lease and optionally -# a lease time. If you have more than one network, you will need to -# repeat this for each network on which you want to supply DHCP -# service. -#dhcp-range=192.168.0.50,192.168.0.150,12h - -# This is an example of a DHCP range where the netmask is given. This -# is needed for networks we reach the dnsmasq DHCP server via a relay -# agent. If you don't know what a DHCP relay agent is, you probably -# don't need to worry about this. -#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h - -# This is an example of a DHCP range which sets a tag, so that -# some DHCP options may be set only for this network. -#dhcp-range=set:red,192.168.0.50,192.168.0.150 - -# Use this DHCP range only when the tag "green" is set. -#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h - -# Specify a subnet which can't be used for dynamic address allocation, -# is available for hosts with matching --dhcp-host lines. Note that -# dhcp-host declarations will be ignored unless there is a dhcp-range -# of some type for the subnet in question. -# In this case the netmask is implied (it comes from the network -# configuration on the machine running dnsmasq) it is possible to give -# an explicit netmask instead. -#dhcp-range=192.168.0.0,static - -# Enable DHCPv6. Note that the prefix-length does not need to be specified -# and defaults to 64 if missing/ -#dhcp-range=1234::2, 1234::500, 64, 12h - -# Do Router Advertisements, BUT NOT DHCP for this subnet. -#dhcp-range=1234::, ra-only - -# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and -# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack -# hosts. Use the DHCPv4 lease to derive the name, network segment and -# MAC address and assume that the host will also have an -# IPv6 address calculated using the SLAAC alogrithm. -#dhcp-range=1234::, ra-names - -# Do Router Advertisements, BUT NOT DHCP for this subnet. -# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.) -#dhcp-range=1234::, ra-only, 48h - -# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA -# so that clients can use SLAAC addresses as well as DHCP ones. -#dhcp-range=1234::2, 1234::500, slaac - -# Do Router Advertisements and stateless DHCP for this subnet. Clients will -# not get addresses from DHCP, but they will get other configuration information. -# They will use SLAAC for addresses. -#dhcp-range=1234::, ra-stateless - -# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses -# from DHCPv4 leases. -#dhcp-range=1234::, ra-stateless, ra-names - -# Do router advertisements for all subnets where we're doing DHCPv6 -# Unless overriden by ra-stateless, ra-names, et al, the router -# advertisements will have the M and O bits set, so that the clients -# get addresses and configuration from DHCPv6, and the A bit reset, so the -# clients don't use SLAAC addresses. -#enable-ra - -# Supply parameters for specified hosts using DHCP. There are lots -# of valid alternatives, so we will give examples of each. Note that -# IP addresses DO NOT have to be in the range given above, they just -# need to be on the same network. The order of the parameters in these -# do not matter, it's permissible to give name, address and MAC in any -# order. - -# Always allocate the host with Ethernet address 11:22:33:44:55:66 -# The IP address 192.168.0.60 -#dhcp-host=11:22:33:44:55:66,192.168.0.60 - -# Always set the name of the host with hardware address -# 11:22:33:44:55:66 to be "fred" -#dhcp-host=11:22:33:44:55:66,fred - -# Always give the host with Ethernet address 11:22:33:44:55:66 -# the name fred and IP address 192.168.0.60 and lease time 45 minutes -#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m - -# Give a host with Ethernet address 11:22:33:44:55:66 or -# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume -# that these two Ethernet interfaces will never be in use at the same -# time, and give the IP address to the second, even if it is already -# in use by the first. Useful for laptops with wired and wireless -# addresses. -#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60 - -# Give the machine which says its name is "bert" IP address -# 192.168.0.70 and an infinite lease -#dhcp-host=bert,192.168.0.70,infinite - -# Always give the host with client identifier 01:02:02:04 -# the IP address 192.168.0.60 -#dhcp-host=id:01:02:02:04,192.168.0.60 - -# Always give the Infiniband interface with hardware address -# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the -# ip address 192.168.0.61. The client id is derived from the prefix -# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of -# hex digits of the hardware address. -#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61 - -# Always give the host with client identifier "marjorie" -# the IP address 192.168.0.60 -#dhcp-host=id:marjorie,192.168.0.60 - -# Enable the address given for "judge" in /etc/hosts -# to be given to a machine presenting the name "judge" when -# it asks for a DHCP lease. -#dhcp-host=judge - -# Never offer DHCP service to a machine whose Ethernet -# address is 11:22:33:44:55:66 -#dhcp-host=11:22:33:44:55:66,ignore - -# Ignore any client-id presented by the machine with Ethernet -# address 11:22:33:44:55:66. This is useful to prevent a machine -# being treated differently when running under different OS's or -# between PXE boot and OS boot. -#dhcp-host=11:22:33:44:55:66,id:* - -# Send extra options which are tagged as "red" to -# the machine with Ethernet address 11:22:33:44:55:66 -#dhcp-host=11:22:33:44:55:66,set:red - -# Send extra options which are tagged as "red" to -# any machine with Ethernet address starting 11:22:33: -#dhcp-host=11:22:33:*:*:*,set:red - -# Give a fixed IPv6 address and name to client with -# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 -# Note the MAC addresses CANNOT be used to identify DHCPv6 clients. -# Note also the they [] around the IPv6 address are obilgatory. -#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] - -# Ignore any clients which are not specified in dhcp-host lines -# or /etc/ethers. Equivalent to ISC "deny unknown-clients". -# This relies on the special "known" tag which is set when -# a host is matched. -#dhcp-ignore=tag:!known - -# Send extra options which are tagged as "red" to any machine whose -# DHCP vendorclass string includes the substring "Linux" -#dhcp-vendorclass=set:red,Linux - -# Send extra options which are tagged as "red" to any machine one -# of whose DHCP userclass strings includes the substring "accounts" -#dhcp-userclass=set:red,accounts - -# Send extra options which are tagged as "red" to any machine whose -# MAC address matches the pattern. -#dhcp-mac=set:red,00:60:8C:*:*:* - -# If this line is uncommented, dnsmasq will read /etc/ethers and act -# on the ethernet-address/IP pairs found there just as if they had -# been given as --dhcp-host options. Useful if you keep -# MAC-address/host mappings there for other purposes. -#read-ethers - -# Send options to hosts which ask for a DHCP lease. -# See RFC 2132 for details of available options. -# Common options can be given to dnsmasq by name: -# run "dnsmasq --help dhcp" to get a list. -# Note that all the common settings, such as netmask and -# broadcast address, DNS server and default route, are given -# sane defaults by dnsmasq. You very likely will not need -# any dhcp-options. If you use Windows clients and Samba, there -# are some options which are recommended, they are detailed at the -# end of this section. - -# Override the default route supplied by dnsmasq, which assumes the -# router is the same machine as the one running dnsmasq. -#dhcp-option=3,1.2.3.4 - -# Do the same thing, but using the option name -#dhcp-option=option:router,1.2.3.4 - -# Override the default route supplied by dnsmasq and send no default -# route at all. Note that this only works for the options sent by -# default (1, 3, 6, 12, 28) the same line will send a zero-length option -# for all other option numbers. -#dhcp-option=3 - -# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5 -#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5 - -# Send DHCPv6 option. Note [] around IPv6 addresses. -#dhcp-option=option6:dns-server,[1234::77],[1234::88] - -# Send DHCPv6 option for namservers as the machine running -# dnsmasq and another. -#dhcp-option=option6:dns-server,[::],[1234::88] - -# Ask client to poll for option changes every six hours. (RFC4242) -#dhcp-option=option6:information-refresh-time,6h - -# Set option 58 client renewal time (T1). Defaults to half of the -# lease time if not specified. (RFC2132) -#dhcp-option=option:T1:1m - -# Set option 59 rebinding time (T2). Defaults to 7/8 of the -# lease time if not specified. (RFC2132) -#dhcp-option=option:T2:2m - -# Set the NTP time server address to be the same machine as -# is running dnsmasq -#dhcp-option=42,0.0.0.0 - -# Set the NIS domain name to "welly" -#dhcp-option=40,welly - -# Set the default time-to-live to 50 -#dhcp-option=23,50 - -# Set the "all subnets are local" flag -#dhcp-option=27,1 - -# Send the etherboot magic flag and then etherboot options (a string). -#dhcp-option=128,e4:45:74:68:00:00 -#dhcp-option=129,NIC=eepro100 - -# Specify an option which will only be sent to the "red" network -# (see dhcp-range for the declaration of the "red" network) -# Note that the tag: part must precede the option: part. -#dhcp-option = tag:red, option:ntp-server, 192.168.1.1 - -# The following DHCP options set up dnsmasq in the same way as is specified -# for the ISC dhcpcd in -# http://www.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt -# adapted for a typical dnsmasq installation where the host running -# dnsmasq is also the host running samba. -# you may want to uncomment some or all of them if you use -# Windows clients and Samba. -#dhcp-option=19,0 # option ip-forwarding off -#dhcp-option=44,0.0.0.0 # set netbios-over-TCP/IP nameserver(s) aka WINS server(s) -#dhcp-option=45,0.0.0.0 # netbios datagram distribution server -#dhcp-option=46,8 # netbios node type - -# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave. -#dhcp-option=252,"\n" - -# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client -# probably doesn't support this...... -#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com - -# Send RFC-3442 classless static routes (note the netmask encoding) -#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8 - -# Send vendor-class specific options encapsulated in DHCP option 43. -# The meaning of the options is defined by the vendor-class so -# options are sent only when the client supplied vendor class -# matches the class given here. (A substring match is OK, so "MSFT" -# matches "MSFT" and "MSFT 5.0"). This example sets the -# mtftp address to 0.0.0.0 for PXEClients. -#dhcp-option=vendor:PXEClient,1,0.0.0.0 - -# Send microsoft-specific option to tell windows to release the DHCP lease -# when it shuts down. Note the "i" flag, to tell dnsmasq to send the -# value as a four-byte integer - that's what microsoft wants. See -# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true -#dhcp-option=vendor:MSFT,2,1i - -# Send the Encapsulated-vendor-class ID needed by some configurations of -# Etherboot to allow is to recognise the DHCP server. -#dhcp-option=vendor:Etherboot,60,"Etherboot" - -# Send options to PXELinux. Note that we need to send the options even -# though they don't appear in the parameter request list, so we need -# to use dhcp-option-force here. -# See http://syslinux.zytor.com/pxe.php#special for details. -# Magic number - needed before anything else is recognised -#dhcp-option-force=208,f1:00:74:7e -# Configuration file name -#dhcp-option-force=209,configs/common -# Path prefix -#dhcp-option-force=210,/tftpboot/pxelinux/files/ -# Reboot time. (Note 'i' to send 32-bit value) -#dhcp-option-force=211,30i - -# Set the boot filename for netboot/PXE. You will only need -# this is you want to boot machines over the network and you will need -# a TFTP server; either dnsmasq's built in TFTP server or an -# external one. (See below for how to enable the TFTP server.) -#dhcp-boot=pxelinux.0 - -# The same as above, but use custom tftp-server instead machine running dnsmasq -#dhcp-boot=pxelinux,server.name,192.168.1.100 - -# Boot for Etherboot gPXE. The idea is to send two different -# filenames, the first loads gPXE, and the second tells gPXE what to -# load. The dhcp-match sets the gpxe tag for requests from gPXE. -#dhcp-match=set:gpxe,175 # gPXE sends a 175 option. -#dhcp-boot=tag:!gpxe,undionly.kpxe -#dhcp-boot=mybootimage - -# Encapsulated options for Etherboot gPXE. All the options are -# encapsulated within option 175 -#dhcp-option=encap:175, 1, 5b # priority code -#dhcp-option=encap:175, 176, 1b # no-proxydhcp -#dhcp-option=encap:175, 177, string # bus-id -#dhcp-option=encap:175, 189, 1b # BIOS drive code -#dhcp-option=encap:175, 190, user # iSCSI username -#dhcp-option=encap:175, 191, pass # iSCSI password - -# Test for the architecture of a netboot client. PXE clients are -# supposed to send their architecture as option 93. (See RFC 4578) -#dhcp-match=peecees, option:client-arch, 0 #x86-32 -#dhcp-match=itanics, option:client-arch, 2 #IA64 -#dhcp-match=hammers, option:client-arch, 6 #x86-64 -#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64 - -# Do real PXE, rather than just booting a single file, this is an -# alternative to dhcp-boot. -#pxe-prompt="What system shall I netboot?" -# or with timeout before first available action is taken: -#pxe-prompt="Press F8 for menu.", 60 - -# Available boot services. for PXE. -#pxe-service=x86PC, "Boot from local disk" - -# Loads /pxelinux.0 from dnsmasq TFTP server. -#pxe-service=x86PC, "Install Linux", pxelinux - -# Loads /pxelinux.0 from TFTP server at 1.2.3.4. -# Beware this fails on old PXE ROMS. -#pxe-service=x86PC, "Install Linux", pxelinux, 1.2.3.4 - -# Use bootserver on network, found my multicast or broadcast. -#pxe-service=x86PC, "Install windows from RIS server", 1 - -# Use bootserver at a known IP address. -#pxe-service=x86PC, "Install windows from RIS server", 1, 1.2.3.4 - -# If you have multicast-FTP available, -# information for that can be passed in a similar way using options 1 -# to 5. See page 19 of -# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf - - -# Enable dnsmasq's built-in TFTP server -#enable-tftp - -# Set the root directory for files available via FTP. -#tftp-root=/var/ftpd - -# Do not abort if the tftp-root is unavailable -#tftp-no-fail - -# Make the TFTP server more secure: with this set, only files owned by -# the user dnsmasq is running as will be send over the net. -#tftp-secure - -# This option stops dnsmasq from negotiating a larger blocksize for TFTP -# transfers. It will slow things down, but may rescue some broken TFTP -# clients. -#tftp-no-blocksize - -# Set the boot file name only when the "red" tag is set. -#dhcp-boot=tag:red,pxelinux.red-net - -# An example of dhcp-boot with an external TFTP server: the name and IP -# address of the server are given after the filename. -# Can fail with old PXE ROMS. Overridden by --pxe-service. -#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3 - -# If there are multiple external tftp servers having a same name -# (using /etc/hosts) then that name can be specified as the -# tftp_servername (the third option to dhcp-boot) and in that -# case dnsmasq resolves this name and returns the resultant IP -# addresses in round robin fasion. This facility can be used to -# load balance the tftp load among a set of servers. -#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name - -# Set the limit on DHCP leases, the default is 150 -#dhcp-lease-max=150 - -# The DHCP server needs somewhere on disk to keep its lease database. -# This defaults to a sane location, but if you want to change it, use -# the line below. -#dhcp-leasefile=/var/lib/misc/dnsmasq.leases - -# Set the DHCP server to authoritative mode. In this mode it will barge in -# and take over the lease for any client which broadcasts on the network, -# whether it has a record of the lease or not. This avoids long timeouts -# when a machine wakes up on a new network. DO NOT enable this if there's -# the slightest chance that you might end up accidentally configuring a DHCP -# server for your campus/company accidentally. The ISC server uses -# the same option, and this URL provides more information: -# http://www.isc.org/files/auth.html -#dhcp-authoritative - -# Run an executable when a DHCP lease is created or destroyed. -# The arguments sent to the script are "add" or "del", -# then the MAC address, the IP address and finally the hostname -# if there is one. -#dhcp-script=/bin/echo - -# Set the cachesize here. -#cache-size=150 - -# If you want to disable negative caching, uncomment this. -#no-negcache - -# Normally responses which come from /etc/hosts and the DHCP lease -# file have Time-To-Live set as zero, which conventionally means -# do not cache further. If you are happy to trade lower load on the -# server for potentially stale date, you can set a time-to-live (in -# seconds) here. -#local-ttl= - -# If you want dnsmasq to detect attempts by Verisign to send queries -# to unregistered .com and .net hosts to its sitefinder service and -# have dnsmasq instead return the correct NXDOMAIN response, uncomment -# this line. You can add similar lines to do the same for other -# registries which have implemented wildcard A records. -#bogus-nxdomain=64.94.110.11 - -# If you want to fix up DNS results from upstream servers, use the -# alias option. This only works for IPv4. -# This alias makes a result of 1.2.3.4 appear as 5.6.7.8 -#alias=1.2.3.4,5.6.7.8 -# and this maps 1.2.3.x to 5.6.7.x -#alias=1.2.3.0,5.6.7.0,255.255.255.0 -# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40 -#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0 - -# Change these lines if you want dnsmasq to serve MX records. - -# Return an MX record named "maildomain.com" with target -# servermachine.com and preference 50 -#mx-host=maildomain.com,servermachine.com,50 - -# Set the default target for MX records created using the localmx option. -#mx-target=servermachine.com - -# Return an MX record pointing to the mx-target for all local -# machines. -#localmx - -# Return an MX record pointing to itself for all local machines. -#selfmx - -# Change the following lines if you want dnsmasq to serve SRV -# records. These are useful if you want to serve ldap requests for -# Active Directory and other windows-originated DNS requests. -# See RFC 2782. -# You may add multiple srv-host lines. -# The fields are ,,,, -# If the domain part if missing from the name (so that is just has the -# service and protocol sections) then the domain given by the domain= -# config option is used. (Note that expand-hosts does not need to be -# set for this to work.) - -# A SRV record sending LDAP for the example.com domain to -# ldapserver.example.com port 389 -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389 - -# A SRV record sending LDAP for the example.com domain to -# ldapserver.example.com port 389 (using domain=) -#domain=example.com -#srv-host=_ldap._tcp,ldapserver.example.com,389 - -# Two SRV records for LDAP, each with different priorities -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1 -#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2 - -# A SRV record indicating that there is no LDAP server for the domain -# example.com -#srv-host=_ldap._tcp.example.com - -# The following line shows how to make dnsmasq serve an arbitrary PTR -# record. This is useful for DNS-SD. (Note that the -# domain-name expansion done for SRV records _does_not -# occur for PTR records.) -#ptr-record=_http._tcp.dns-sd-services,"New Employee Page._http._tcp.dns-sd-services" - -# Change the following lines to enable dnsmasq to serve TXT records. -# These are used for things like SPF and zeroconf. (Note that the -# domain-name expansion done for SRV records _does_not -# occur for TXT records.) - -#Example SPF. -#txt-record=example.com,"v=spf1 a -all" - -#Example zeroconf -#txt-record=_http._tcp.example.com,name=value,paper=A4 - -# Provide an alias for a "local" DNS name. Note that this _only_ works -# for targets which are names from DHCP or /etc/hosts. Give host -# "bert" another name, bertrand -#cname=bertand,bert - -# For debugging purposes, log each DNS query as it passes through -# dnsmasq. -#log-queries - -# Log lots of extra information about DHCP transactions. -#log-dhcp - -# Include another lot of configuration options. -#conf-file=/etc/dnsmasq.more.conf -#conf-dir=/etc/dnsmasq.d - -# Include all the files in a directory except those ending in .bak -#conf-dir=/etc/dnsmasq.d,.bak - -# Include all files in a directory which end in .conf -#conf-dir=/etc/dnsmasq.d/,*.conf -# diff --git a/roles/features/templates/pagespeed.conf.j2 b/roles/features/templates/pagespeed.conf.j2 deleted file mode 100644 index 3b89b758..00000000 --- a/roles/features/templates/pagespeed.conf.j2 +++ /dev/null @@ -1,369 +0,0 @@ - - # Turn on mod_pagespeed. To completely disable mod_pagespeed, you - # can set this to "off". - ModPagespeed on - - # We want VHosts to inherit global configuration. - # If this is not included, they'll be independent (except for inherently - # global options), at least for backwards compatibility. - ModPagespeedInheritVHostConfig on - - # Direct Apache to send all HTML output to the mod_pagespeed - # output handler. - AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html - - # If you want mod_pagespeed process XHTML as well, please uncomment this - # line. - # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml - - # The ModPagespeedFileCachePath directory must exist and be writable - # by the apache user (as specified by the User directive). - ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" - - # LogDir is needed to store various logs, including the statistics log - # required for the console. - ModPagespeedLogDir "/var/log/pagespeed" - - # The locations of SSL Certificates is distribution-dependent. - ModPagespeedSslCertDirectory "/etc/ssl/certs" - - - # If you want, you can use one or more memcached servers as the store for - # the mod_pagespeed cache. - # ModPagespeedMemcachedServers localhost:11211 - - # A portion of the cache can be kept in memory only, to reduce load on disk - # (or memcached) from many small files. - # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 - - # Override the mod_pagespeed 'rewrite level'. The default level - # "CoreFilters" uses a set of rewrite filters that are generally - # safe for most web pages. Most sites should not need to change - # this value and can instead fine-tune the configuration using the - # ModPagespeedDisableFilters and ModPagespeedEnableFilters - # directives, below. Valid values for ModPagespeedRewriteLevel are - # PassThrough, CoreFilters and TestingCoreFilters. - # - ModPagespeedRewriteLevel CoreFilters - - ModPagespeedEnableFilters combine_heads - ModPagespeedEnableFilters combine_javascript - ModPagespeedEnableFilters convert_jpeg_to_webp - ModPagespeedEnableFilters convert_png_to_jpeg - ModPagespeedEnableFilters inline_preview_images - ModPagespeedEnableFilters make_google_analytics_async - ModPagespeedEnableFilters move_css_above_scripts - ModPagespeedEnableFilters move_css_to_head - ModPagespeedEnableFilters resize_mobile_images - ModPagespeedEnableFilters sprite_images - - ModPagespeedEnableFilters defer_iframe - ModPagespeedEnableFilters defer_javascript - ModPagespeedEnableFilters lazyload_images - - # Explicitly disables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, if one - # of the filters in the CoreFilters needs to be disabled for a - # site, that filter can be added to - # ModPagespeedDisableFilters. This directive contains a - # comma-separated list of filter names, and can be repeated. - # - # ModPagespeedDisableFilters rewrite_images - - # Explicitly enables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, filters - # not included in the CoreFilters may be enabled using this - # directive. This directive contains a comma-separated list of - # filter names, and can be repeated. - # - # ModPagespeedEnableFilters rewrite_javascript,rewrite_css - # ModPagespeedEnableFilters collapse_whitespace,elide_attributes - - # Explicitly forbids the enabling of specific filters using either query - # parameters or request headers. This is useful, for example, when we do - # not want the filter to run for performance or security reasons. This - # directive contains a comma-separated list of filter names, and can be - # repeated. - # - # ModPagespeedForbidFilters rewrite_images - - # How long mod_pagespeed will wait to return an optimized resource - # (per flush window) on first request before giving up and returning the - # original (unoptimized) resource. After this deadline is exceeded the - # original resource is returned and the optimization is pushed to the - # background to be completed for future requests. Increasing this value will - # increase page latency, but might reduce load time (for instance on a - # bandwidth-constrained link where it's worth waiting for image - # compression to complete). If the value is less than or equal to zero - # mod_pagespeed will wait indefinitely for the rewrite to complete before - # returning. - # - # ModPagespeedRewriteDeadlinePerFlushMs 10 - - # ModPagespeedDomain - # authorizes rewriting of JS, CSS, and Image files found in this - # domain. By default only resources with the same origin as the - # HTML file are rewritten. For example: - # - ModPagespeedDomain * - # - # This will allow resources found on http://cdn.myhost.com to be - # rewritten in addition to those in the same domain as the HTML. - # - # Other domain-related directives (like ModPagespeedMapRewriteDomain - # and ModPagespeedMapOriginDomain) can also authorize domains. - # - # Wildcards (* and ?) are allowed in the domain specification. Be - # careful when using them as if you rewrite domains that do not - # send you traffic, then the site receiving the traffic will not - # know how to serve the rewritten content. - - # If you use downstream caches such as varnish or proxy_cache for caching - # HTML, you can configure pagespeed to work with these caches correctly - # using the following directives. Note that the values for - # ModPagespeedDownstreamCachePurgeLocationPrefix and - # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here - # in order to force the webmaster to choose appropriate value for these. - # - # ModPagespeedDownstreamCachePurgeLocationPrefix - # ModPagespeedDownstreamCachePurgeMethod PURGE - # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 - # ModPagespeedDownstreamCacheRebeaconingKey - - # Other defaults (cache sizes and thresholds): - # - # ModPagespeedFileCacheSizeKb 102400 - # ModPagespeedFileCacheCleanIntervalMs 3600000 - # ModPagespeedLRUCacheKbPerProcess 1024 - # ModPagespeedLRUCacheByteLimit 16384 - # ModPagespeedCssFlattenMaxBytes 102400 - # ModPagespeedCssInlineMaxBytes 2048 - # ModPagespeedCssImageInlineMaxBytes 0 - # ModPagespeedImageInlineMaxBytes 3072 - # ModPagespeedJsInlineMaxBytes 2048 - # ModPagespeedCssOutlineMinBytes 3000 - # ModPagespeedJsOutlineMinBytes 3000 - # ModPagespeedMaxCombinedCssBytes -1 - # ModPagespeedMaxCombinedJsBytes 92160 - - # Limit the number of inodes in the file cache. Set to 0 for no limit. - # The default value if this paramater is not specified is 0 (no limit). - ModPagespeedFileCacheInodeLimit 500000 - - # Bound the number of images that can be rewritten at any one time; this - # avoids overloading the CPU. Set this to 0 to remove the bound. - # - # ModPagespeedImageMaxRewritesAtOnce 8 - - # You can also customize the number of threads per Apache process - # mod_pagespeed will use to do resource optimization. Plain - # "rewrite threads" are used to do short, latency-sensitive work, - # while "expensive rewrite threads" are used for actual optimization - # work that's more computationally expensive. If you live these unset, - # or use values <= 0 the defaults will be used, which is 1 for both - # values when using non-threaded MPMs (e.g. prefork) and 4 for both - # on threaded MPMs (e.g. worker and event). These settings can only - # be changed globally, and not per virtual host. - # - # ModPagespeedNumRewriteThreads 4 - # ModPagespeedNumExpensiveRewriteThreads 4 - - # Randomly drop rewrites (*) to increase the chance of optimizing - # frequently fetched resources and decrease the chance of optimizing - # infrequently fetched resources. This can reduce CPU load. The default - # value of this parameter is 0 (no drops). 90 means that a resourced - # fetched once has a 10% probability of being optimized while a resource - # that is fetched 50 times has a 99.65% probability of being optimized. - # - # (*) Currently only CSS files and images are randomly dropped. Images - # within CSS files are not randomly dropped. - # - # ModPagespeedRewriteRandomDropPercentage 90 - - # Many filters modify the URLs of resources in HTML files. This is typically - # harmless but pages whose Javascript expects to read or modify the original - # URLs may break. The following parameters prevent filters from modifying - # URLs of their respective types. - # - # ModPagespeedJsPreserveURLs on - # ModPagespeedImagePreserveURLs on - # ModPagespeedCssPreserveURLs on - - # When PreserveURLs is on, it is still possible to enable browser-specific - # optimizations (for example, webp images can be served to browsers that - # will accept them). They'll be served with Vary: Accept or Vary: - # User-Agent headers as appropriate. Note that this may require configuring - # reverse proxy caches such as varnish to handle these headers properly. - # - # ModPagespeedFilters in_place_optimize_for_browser - - # Internet Explorer has difficulty caching resources with Vary: headers. - # They will either be uncached (older IE) or require revalidation. See: - # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx - # As a result we serve them as Cache-Control: private instead by default. - # If you are using a reverse proxy or CDN configured to cache content with - # the Vary: Accept header you should turn this setting off. - # - # ModPagespeedPrivateNotVaryForIE on - - # Settings for image optimization: - # - # Lossy image recompression quality (0 to 100, -1 just strips metadata): - # ModPagespeedImageRecompressionQuality 85 - # - # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedJpegRecompressionQuality -1 - # ModPagespeedJpegRecompressionQualityForSmallScreens 70 - - ModPagespeedJpegRecompressionQuality 75 - - # - # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedWebpRecompressionQuality 80 - # ModPagespeedWebpRecompressionQualityForSmallScreens 70 - # - # Timeout for conversions to WebP format, in - # milliseconds. Negative values mean no timeout is applied. The - # default value is -1: - # ModPagespeedWebpTimeoutMs 5000 - # - # Percent of original image size below which optimized images are retained: - # ModPagespeedImageLimitOptimizedPercent 100 - # - # Percent of original image area below which image resizing will be - # attempted: - # ModPagespeedImageLimitResizeAreaPercent 100 - - # Settings for inline preview images - # - # Setting this to n restricts preview images to the first n images found on - # the page. The default of -1 means preview images can appear anywhere on - # the page (if those images appear above the fold). - # ModPagespeedMaxInlinedPreviewImagesIndex -1 - - # Sets the minimum size in bytes of any image for which a low quality image - # is generated. - # ModPagespeedMinImageSizeLowResolutionBytes 3072 - - # The maximum URL size is generally limited to about 2k characters - # due to IE: See http://support.microsoft.com/kb/208427/EN-US. - # Apache servers by default impose a further limitation of about - # 250 characters per URL segment (text between slashes). - # mod_pagespeed circumvents this limitation, but if you employ - # proxy servers in your path you may need to re-impose it by - # overriding the setting here. The default setting is 1024 - # characters. - # - # ModPagespeedMaxSegmentLength 250 - - # Uncomment this if you want to prevent mod_pagespeed from combining files - # (e.g. CSS files) across paths - # - # ModPagespeedCombineAcrossPaths off - - # Renaming JavaScript URLs can sometimes break them. With this - # option enabled, mod_pagespeed uses a simple heuristic to decide - # not to rename JavaScript that it thinks is introspective. - # - # You can uncomment this to let mod_pagespeed rename all JS files. - # - # ModPagespeedAvoidRenamingIntrospectiveJavascript off - - # Certain common JavaScript libraries are available from Google, which acts - # as a CDN and allows you to benefit from browser caching if a new visitor - # to your site previously visited another site that makes use of the same - # libraries as you do. Enable the following filter to turn on this feature. - # - # ModPagespeedEnableFilters canonicalize_javascript_libraries - - # The following line configures a library that is recognized by - # canonicalize_javascript_libraries. This will have no effect unless you - # enable this filter (generally by uncommenting the last line in the - # previous stanza). The format is: - # ModPagespeedLibrary bytes md5 canonical_url - # Where bytes and md5 are with respect to the *minified* JS; use - # js_minify --print_size_and_hash to obtain this data. - # Note that we can register multiple hashes for the same canonical url; - # we do this if there are versions available that have already been minified - # with more sophisticated tools. - # - # Additional library configuration can be found in - # pagespeed_libraries.conf included in the distribution. You should add - # new entries here, though, so that file can be automatically upgraded. - # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js - - # Explicitly tell mod_pagespeed to load some resources from disk. - # This will speed up load time and update frequency. - # - # This should only be used for static resources which do not need - # specific headers set or other processing by Apache. - # - # Both URL and filesystem path should specify directories and - # filesystem path must be absolute (for now). - # - # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" - - - # Enables server-side instrumentation and statistics. If this rewriter is - # enabled, then each rewritten HTML page will have instrumentation javacript - # added that sends latency beacons to /mod_pagespeed_beacon. These - # statistics can be accessed at /mod_pagespeed_statistics. You must also - # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers - # below. - # - # ModPagespeedEnableFilters add_instrumentation - - # The add_instrumentation filter sends a beacon after the page onload - # handler is called. The user might navigate to a new URL before this. If - # you enable the following directive, the beacon is sent as part of an - # onbeforeunload handler, for pages where navigation happens before the - # onload event. - # - # ModPagespeedReportUnloadTime on - - # Uncomment the following line so that ModPagespeed will not cache or - # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. - # Note that ModPagespeed always respects Vary: headers on html content. - # ModPagespeedRespectVary on - - # Uncomment the following line if you want to disable statistics entirely. - # - # ModPagespeedStatistics off - - # These handlers are central entry-points into the admin pages. - # By default, pagespeed_admin and pagespeed_global_admin present - # the same data, and differ only when - # ModPagespeedUsePerVHostStatistics is enabled. In that case, - # /pagespeed_global_admin sees aggregated data across all vhosts, - # and the /pagespeed_admin sees data only for a particular vhost. - # - # You may insert other "Allow from" lines to add hosts you want to - # allow to look at generated statistics. Another possibility is - # to comment out the "Order" and "Allow" options from the config - # file, to allow any client that can reach your server to access - # and change server state, such as statistics, caches, and - # messages. This might be appropriate in an experimental setup. - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_admin - - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_global_admin - - - # Enable logging of mod_pagespeed statistics, needed for the console. - ModPagespeedStatisticsLogging on - - # Page /mod_pagespeed_message lets you view the latest messages from - # mod_pagespeed, regardless of log-level in your httpd.conf - # ModPagespeedMessageBufferSize is the maximum number of bytes you would - # like to dump to your /mod_pagespeed_message page at one time, - # its default value is 100k bytes. - # Set it to 0 if you want to disable this feature. - ModPagespeedMessageBufferSize 100000 - diff --git a/roles/features/templates/ports.conf.j2 b/roles/features/templates/ports.conf.j2 deleted file mode 100644 index 2618436c..00000000 --- a/roles/features/templates/ports.conf.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# If you just change the port or add more ports here, you will likely also -# have to change the VirtualHost statement in -# /etc/apache2/sites-enabled/000-default.conf - -Listen 172.16.0.1:8080 - - - Listen 172.16.0.1:443 - - - - Listen 172.16.0.1:443 - diff --git a/roles/features/templates/privoxy_config.j2 b/roles/features/templates/privoxy_config.j2 deleted file mode 100644 index dd55f0f3..00000000 --- a/roles/features/templates/privoxy_config.j2 +++ /dev/null @@ -1,2107 +0,0 @@ -# Sample Configuration File for Privoxy -# -# Id: config,v -# -# Copyright (C) 2001-2014 Privoxy Developers http://www.privoxy.org/ -# -#################################################################### -# # -# Table of Contents # -# # -# I. INTRODUCTION # -# II. FORMAT OF THE CONFIGURATION FILE # -# # -# 1. LOCAL SET-UP DOCUMENTATION # -# 2. CONFIGURATION AND LOG FILE LOCATIONS # -# 3. DEBUGGING # -# 4. ACCESS CONTROL AND SECURITY # -# 5. FORWARDING # -# 6. MISCELLANEOUS # -# 7. WINDOWS GUI OPTIONS # -# # -#################################################################### -# -# -# I. INTRODUCTION -# =============== -# -# This file holds Privoxy's main configuration. Privoxy detects -# configuration changes automatically, so you don't have to restart -# it unless you want to load a different configuration file. -# -# The configuration will be reloaded with the first request after -# the change was done, this request itself will still use the old -# configuration, though. In other words: it takes two requests -# before you see the result of your changes. Requests that are -# dropped due to ACL don't trigger reloads. -# -# When starting Privoxy on Unix systems, give the location of this -# file as last argument. On Windows systems, Privoxy will look for -# this file with the name 'config.txt' in the current working -# directory of the Privoxy process. -# -# -# II. FORMAT OF THE CONFIGURATION FILE -# ==================================== -# -# Configuration lines consist of an initial keyword followed by a -# list of values, all separated by whitespace (any number of spaces -# or tabs). For example, -# -# actionsfile default.action -# -# Indicates that the actionsfile is named 'default.action'. -# -# The '#' indicates a comment. Any part of a line following a '#' is -# ignored, except if the '#' is preceded by a '\'. -# -# Thus, by placing a # at the start of an existing configuration -# line, you can make it a comment and it will be treated as if it -# weren't there. This is called "commenting out" an option and can -# be useful. Removing the # again is called "uncommenting". -# -# Note that commenting out an option and leaving it at its default -# are two completely different things! Most options behave very -# differently when unset. See the "Effect if unset" explanation in -# each option's description for details. -# -# Long lines can be continued on the next line by using a `\' as the -# last character. -# -# -# 1. LOCAL SET-UP DOCUMENTATION -# ============================== -# -# If you intend to operate Privoxy for more users than just -# yourself, it might be a good idea to let them know how to reach -# you, what you block and why you do that, your policies, etc. -# -# -# 1.1. user-manual -# ================= -# -# Specifies: -# -# Location of the Privoxy User Manual. -# -# Type of value: -# -# A fully qualified URI -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# http://www.privoxy.org/version/user-manual/ will be used, -# where version is the Privoxy version. -# -# Notes: -# -# The User Manual URI is the single best source of information -# on Privoxy, and is used for help links from some of the -# internal CGI pages. The manual itself is normally packaged -# with the binary distributions, so you probably want to set -# this to a locally installed copy. -# -# Examples: -# -# The best all purpose solution is simply to put the full local -# PATH to where the User Manual is located: -# -# user-manual /usr/share/doc/privoxy/user-manual -# -# The User Manual is then available to anyone with access to -# Privoxy, by following the built-in URL: http:// -# config.privoxy.org/user-manual/ (or the shortcut: http://p.p/ -# user-manual/). -# -# If the documentation is not on the local system, it can be -# accessed from a remote server, as: -# -# user-manual http://example.com/privoxy/user-manual/ -# -# WARNING!!! -# -# If set, this option should be the first option in the -# config file, because it is used while the config file is -# being read. -# -user-manual /usr/share/doc/privoxy/user-manual -# -# 1.2. trust-info-url -# ==================== -# -# Specifies: -# -# A URL to be displayed in the error page that users will see if -# access to an untrusted page is denied. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No links are displayed on the "untrusted" error page. -# -# Notes: -# -# The value of this option only matters if the experimental -# trust mechanism has been activated. (See trustfile below.) -# -# If you use the trust mechanism, it is a good idea to write up -# some on-line documentation about your trust policy and to -# specify the URL(s) here. Use multiple times for multiple URLs. -# -# The URL(s) should be added to the trustfile as well, so users -# don't end up locked out from the information on why they were -# locked out in the first place! -# -#trust-info-url http://www.example.com/why_we_block.html -#trust-info-url http://www.example.com/what_we_allow.html -# -# 1.3. admin-address -# =================== -# -# Specifies: -# -# An email address to reach the Privoxy administrator. -# -# Type of value: -# -# Email address -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No email address is displayed on error pages and the CGI user -# interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -#admin-address privoxy-admin@example.com -# -# 1.4. proxy-info-url -# ==================== -# -# Specifies: -# -# A URL to documentation about the local Privoxy setup, -# configuration or policies. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No link to local documentation is displayed on error pages and -# the CGI user interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -# This URL shouldn't be blocked ;-) -# -#proxy-info-url http://www.example.com/proxy-service.html -# -# 2. CONFIGURATION AND LOG FILE LOCATIONS -# ======================================== -# -# Privoxy can (and normally does) use a number of other files for -# additional configuration, help and logging. This section of the -# configuration file tells Privoxy where to find those other files. -# -# The user running Privoxy, must have read permission for all -# configuration files, and write permission to any files that would -# be modified, such as log files and actions files. -# -# -# 2.1. confdir -# ============= -# -# Specifies: -# -# The directory where the other configuration files are located. -# -# Type of value: -# -# Path name -# -# Default value: -# -# /etc/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -confdir /etc/privoxy -# -# 2.2. templdir -# ============== -# -# Specifies: -# -# An alternative directory where the templates are loaded from. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# The templates are assumed to be located in confdir/template. -# -# Notes: -# -# Privoxy's original templates are usually overwritten with each -# update. Use this option to relocate customized templates that -# should be kept. As template variables might change between -# updates, you shouldn't expect templates to work with Privoxy -# releases other than the one they were part of, though. -# -#templdir . -# -# 2.3. temporary-directory -# ========================= -# -# Specifies: -# -# A directory where Privoxy can create temporary files. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# No temporary files are created, external filters don't work. -# -# Notes: -# -# To execute external filters, Privoxy has to create temporary -# files. This directive specifies the directory the temporary -# files should be written to. -# -# It should be a directory only Privoxy (and trusted users) can -# access. -# -#temporary-directory . -# -# 2.4. logdir -# ============ -# -# Specifies: -# -# The directory where all logging takes place (i.e. where the -# logfile is located). -# -# Type of value: -# -# Path name -# -# Default value: -# -# /var/log/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -logdir /var/log/privoxy -# -# 2.5. actionsfile -# ================= -# -# Specifies: -# -# The actions file(s) to use -# -# Type of value: -# -# Complete file name, relative to confdir -# -# Default values: -# -# match-all.action # Actions that are applied to all sites and maybe overruled later on. -# -# default.action # Main actions file -# -# user.action # User customizations -# -# Effect if unset: -# -# No actions are taken at all. More or less neutral proxying. -# -# Notes: -# -# Multiple actionsfile lines are permitted, and are in fact -# recommended! -# -# The default values are default.action, which is the "main" -# actions file maintained by the developers, and user.action, -# where you can make your personal additions. -# -# Actions files contain all the per site and per URL -# configuration for ad blocking, cookie management, privacy -# considerations, etc. -# -actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on. -actionsfile default.action # Main actions file -actionsfile user.action # User customizations -# -# 2.6. filterfile -# ================ -# -# Specifies: -# -# The filter file(s) to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# default.filter (Unix) or default.filter.txt (Windows) -# -# Effect if unset: -# -# No textual content filtering takes place, i.e. all +filter{name} -# actions in the actions files are turned neutral. -# -# Notes: -# -# Multiple filterfile lines are permitted. -# -# The filter files contain content modification rules that use -# regular expressions. These rules permit powerful changes on -# the content of Web pages, and optionally the headers as well, -# e.g., you could try to disable your favorite JavaScript -# annoyances, re-write the actual displayed text, or just have -# some fun playing buzzword bingo with web pages. -# -# The +filter{name} actions rely on the relevant filter (name) -# to be defined in a filter file! -# -# A pre-defined filter file called default.filter that contains -# a number of useful filters for common problems is included in -# the distribution. See the section on the filter action for a -# list. -# -# It is recommended to place any locally adapted filters into a -# separate file, such as user.filter. -# -filterfile default.filter -filterfile user.filter # User customizations -# -# 2.7. logfile -# ============= -# -# Specifies: -# -# The log file to use -# -# Type of value: -# -# File name, relative to logdir -# -# Default value: -# -# Unset (commented out). When activated: logfile (Unix) or -# privoxy.log (Windows). -# -# Effect if unset: -# -# No logfile is written. -# -# Notes: -# -# The logfile is where all logging and error messages are -# written. The level of detail and number of messages are set -# with the debug option (see below). The logfile can be useful -# for tracking down a problem with Privoxy (e.g., it's not -# blocking an ad you think it should block) and it can help you -# to monitor what your browser is doing. -# -# Depending on the debug options below, the logfile may be a -# privacy risk if third parties can get access to it. As most -# users will never look at it, Privoxy only logs fatal errors by -# default. -# -# For most troubleshooting purposes, you will have to change -# that, please refer to the debugging section for details. -# -# Any log files must be writable by whatever user Privoxy is -# being run as (on Unix, default user id is "privoxy"). -# -# To prevent the logfile from growing indefinitely, it is -# recommended to periodically rotate or shorten it. Many -# operating systems support log rotation out of the box, some -# require additional software to do it. For details, please -# refer to the documentation for your operating system. -# -logfile logfile -# -# 2.8. trustfile -# =============== -# -# Specifies: -# -# The name of the trust file to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# Unset (commented out). When activated: trust (Unix) or -# trust.txt (Windows) -# -# Effect if unset: -# -# The entire trust mechanism is disabled. -# -# Notes: -# -# The trust mechanism is an experimental feature for building -# white-lists and should be used with care. It is NOT -# recommended for the casual user. -# -# If you specify a trust file, Privoxy will only allow access to -# sites that are specified in the trustfile. Sites can be listed -# in one of two ways: -# -# Prepending a ~ character limits access to this site only (and -# any sub-paths within this site), e.g. ~www.example.com allows -# access to ~www.example.com/features/news.html, etc. -# -# Or, you can designate sites as trusted referrers, by -# prepending the name with a + character. The effect is that -# access to untrusted sites will be granted -- but only if a -# link from this trusted referrer was used to get there. The -# link target will then be added to the "trustfile" so that -# future, direct accesses will be granted. Sites added via this -# mechanism do not become trusted referrers themselves (i.e. -# they are added with a ~ designation). There is a limit of 512 -# such entries, after which new entries will not be made. -# -# If you use the + operator in the trust file, it may grow -# considerably over time. -# -# It is recommended that Privoxy be compiled with the -# --disable-force, --disable-toggle and --disable-editor -# options, if this feature is to be used. -# -# Possible applications include limiting Internet access for -# children. -# -#trustfile trust -# -# 3. DEBUGGING -# ============= -# -# These options are mainly useful when tracing a problem. Note that -# you might also want to invoke Privoxy with the --no-daemon command -# line option when debugging. -# -# -# 3.1. debug -# =========== -# -# Specifies: -# -# Key values that determine what information gets logged. -# -# Type of value: -# -# Integer values -# -# Default value: -# -# 0 (i.e.: only fatal errors (that cause Privoxy to exit) are -# logged) -# -# Effect if unset: -# -# Default value is used (see above). -# -# Notes: -# -# The available debug levels are: -# -# debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -# debug 2 # show each connection status -# debug 4 # show I/O status -# debug 8 # show header parsing -# debug 16 # log all data written to the network -# debug 32 # debug force feature -# debug 64 # debug regular expression filters -# debug 128 # debug redirects -# debug 256 # debug GIF de-animation -# debug 512 # Common Log Format -# debug 1024 # Log the destination for requests Privoxy didn't let through, and the reason why. -# debug 2048 # CGI user interface -# debug 4096 # Startup banner and warnings. -# debug 8192 # Non-fatal errors -# debug 32768 # log all data read from the network -# debug 65536 # Log the applying actions -# -# To select multiple debug levels, you can either add them or -# use multiple debug lines. -# -# A debug level of 1 is informative because it will show you -# each request as it happens. 1, 1024, 4096 and 8192 are -# recommended so that you will notice when things go wrong. The -# other levels are probably only of interest if you are hunting -# down a specific problem. They can produce a hell of an output -# (especially 16). -# -# If you are used to the more verbose settings, simply enable -# the debug lines below again. -# -# If you want to use pure CLF (Common Log Format), you should -# set "debug 512" ONLY and not enable anything else. -# -# Privoxy has a hard-coded limit for the length of log messages. -# If it's reached, messages are logged truncated and marked with -# "... [too long, truncated]". -# -# Please don't file any support requests without trying to -# reproduce the problem with increased debug level first. Once -# you read the log messages, you may even be able to solve the -# problem on your own. -# -#debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -#debug 1024 # Actions that are applied to all sites and maybe overruled later on. -#debug 4096 # Startup banner and warnings -#debug 8192 # Non-fatal errors -# -# 3.2. single-threaded -# ===================== -# -# Specifies: -# -# Whether to run only one server thread. -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Multi-threaded (or, where unavailable: forked) operation, i.e. -# the ability to serve multiple requests simultaneously. -# -# Notes: -# -# This option is only there for debugging purposes. It will -# drastically reduce performance. -# -#single-threaded 1 -# -# 3.3. hostname -# ============== -# -# Specifies: -# -# The hostname shown on the CGI pages. -# -# Type of value: -# -# Text -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# The hostname provided by the operating system is used. -# -# Notes: -# -# On some misconfigured systems resolving the hostname fails or -# takes too much time and slows Privoxy down. Setting a fixed -# hostname works around the problem. -# -# In other circumstances it might be desirable to show a -# hostname other than the one returned by the operating system. -# For example if the system has several different hostnames and -# you don't want to use the first one. -# -# Note that Privoxy does not validate the specified hostname -# value. -# -#hostname hostname.example.org -# -# 4. ACCESS CONTROL AND SECURITY -# =============================== -# -# This section of the config file controls the security-relevant -# aspects of Privoxy's configuration. -# -# -# 4.1. listen-address -# ==================== -# -# Specifies: -# -# The address and TCP port on which Privoxy will listen for -# client requests. -# -# Type of value: -# -# [IP-Address]:Port -# -# [Hostname]:Port -# -# Default value: -# -# 127.0.0.1:8118 -# -# Effect if unset: -# -# Bind to 127.0.0.1 (IPv4 localhost), port 8118. This is -# suitable and recommended for home users who run Privoxy on the -# same machine as their browser. -# -# Notes: -# -# You will need to configure your browser(s) to this proxy -# address and port. -# -# If you already have another service running on port 8118, or -# if you want to serve requests from other machines (e.g. on -# your local network) as well, you will need to override the -# default. -# -# You can use this statement multiple times to make Privoxy -# listen on more ports or more IP addresses. Suitable if your -# operating system does not support sharing IPv6 and IPv4 -# protocols on the same socket. -# -# If a hostname is used instead of an IP address, Privoxy will -# try to resolve it to an IP address and if there are multiple, -# use the first one returned. -# -# If the address for the hostname isn't already known on the -# system (for example because it's in /etc/hostname), this may -# result in DNS traffic. -# -# If the specified address isn't available on the system, or if -# the hostname can't be resolved, Privoxy will fail to start. -# -# IPv6 addresses containing colons have to be quoted by -# brackets. They can only be used if Privoxy has been compiled -# with IPv6 support. If you aren't sure if your version supports -# it, have a look at http://config.privoxy.org/show-status. -# -# Some operating systems will prefer IPv6 to IPv4 addresses even -# if the system has no IPv6 connectivity which is usually not -# expected by the user. Some even rely on DNS to resolve -# localhost which mean the "localhost" address used may not -# actually be local. -# -# It is therefore recommended to explicitly configure the -# intended IP address instead of relying on the operating -# system, unless there's a strong reason not to. -# -# If you leave out the address, Privoxy will bind to all IPv4 -# interfaces (addresses) on your machine and may become -# reachable from the Internet and/or the local network. Be aware -# that some GNU/Linux distributions modify that behaviour -# without updating the documentation. Check for non-standard -# patches if your Privoxy version behaves differently. -# -# If you configure Privoxy to be reachable from the network, -# consider using access control lists (ACL's, see below), and/or -# a firewall. -# -# If you open Privoxy to untrusted users, you will also want to -# make sure that the following actions are disabled: -# enable-edit-actions and enable-remote-toggle -# -# Example: -# -# Suppose you are running Privoxy on a machine which has the -# address 192.168.0.1 on your local private network -# (192.168.0.0) and has another outside connection with a -# different address. You want it to serve requests from inside -# only: -# -# listen-address 192.168.0.1:8118 -# -# Suppose you are running Privoxy on an IPv6-capable machine and -# you want it to listen on the IPv6 address of the loopback -# device: -# -# listen-address [::1]:8118 -# -# -listen-address 172.16.0.1:8118 -# -# 4.2. toggle -# ============ -# -# Specifies: -# -# Initial state of "toggle" status -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 1 -# -# Effect if unset: -# -# Act as if toggled on -# -# Notes: -# -# If set to 0, Privoxy will start in "toggled off" mode, i.e. -# mostly behave like a normal, content-neutral proxy with both -# ad blocking and content filtering disabled. See -# enable-remote-toggle below. -# -toggle 1 -# -# 4.3. enable-remote-toggle -# ========================== -# -# Specifies: -# -# Whether or not the web-based toggle feature may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based toggle feature is disabled. -# -# Notes: -# -# When toggled off, Privoxy mostly acts like a normal, -# content-neutral proxy, i.e. doesn't block ads or filter -# content. -# -# Access to the toggle feature can not be controlled separately -# by "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# toggle it for all users. So this option is not recommended for -# multi-user environments with untrusted users. -# -# Note that malicious client side code (e.g Java) is also -# capable of using this option. -# -# As a lot of Privoxy users don't read documentation, this -# feature is disabled by default. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-remote-toggle 0 -# -# 4.4. enable-remote-http-toggle -# =============================== -# -# Specifies: -# -# Whether or not Privoxy recognizes special HTTP headers to -# change its behaviour. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores special HTTP headers. -# -# Notes: -# -# When toggled on, the client can change Privoxy's behaviour by -# setting special HTTP headers. Currently the only supported -# special header is "X-Filter: No", to disable filtering for the -# ongoing request, even if it is enabled in one of the action -# files. -# -# This feature is disabled by default. If you are using Privoxy -# in a environment with trusted clients, you may enable this -# feature at your discretion. Note that malicious client side -# code (e.g Java) is also capable of using this feature. -# -# This option will be removed in future releases as it has been -# obsoleted by the more general header taggers. -# -enable-remote-http-toggle 0 -# -# 4.5. enable-edit-actions -# ========================= -# -# Specifies: -# -# Whether or not the web-based actions file editor may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based actions file editor is disabled. -# -# Notes: -# -# Access to the editor can not be controlled separately by -# "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# modify its configuration for all users. -# -# This option is not recommended for environments with untrusted -# users and as a lot of Privoxy users don't read documentation, -# this feature is disabled by default. -# -# Note that malicious client side code (e.g Java) is also -# capable of using the actions editor and you shouldn't enable -# this options unless you understand the consequences and are -# sure your browser is configured correctly. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-edit-actions 0 -# -# 4.6. enforce-blocks -# ==================== -# -# Specifies: -# -# Whether the user is allowed to ignore blocks and can "go there -# anyway". -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Blocks are not enforced. -# -# Notes: -# -# Privoxy is mainly used to block and filter requests as a -# service to the user, for example to block ads and other junk -# that clogs the pipes. Privoxy's configuration isn't perfect -# and sometimes innocent pages are blocked. In this situation it -# makes sense to allow the user to enforce the request and have -# Privoxy ignore the block. -# -# In the default configuration Privoxy's "Blocked" page contains -# a "go there anyway" link to adds a special string (the force -# prefix) to the request URL. If that link is used, Privoxy will -# detect the force prefix, remove it again and let the request -# pass. -# -# Of course Privoxy can also be used to enforce a network -# policy. In that case the user obviously should not be able to -# bypass any blocks, and that's what the "enforce-blocks" option -# is for. If it's enabled, Privoxy hides the "go there anyway" -# link. If the user adds the force prefix by hand, it will not -# be accepted and the circumvention attempt is logged. -# -# Examples: -# -# enforce-blocks 1 -# -enforce-blocks 0 -# -# 4.7. ACLs: permit-access and deny-access -# ========================================= -# -# Specifies: -# -# Who can access what. -# -# Type of value: -# -# src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]] -# -# Where src_addr and dst_addr are IPv4 addresses in dotted -# decimal notation or valid DNS names, port is a port number, -# and src_masklen and dst_masklen are subnet masks in CIDR -# notation, i.e. integer values from 2 to 30 representing the -# length (in bits) of the network address. The masks and the -# whole destination part are optional. -# -# If your system implements RFC 3493, then src_addr and dst_addr -# can be IPv6 addresses delimeted by brackets, port can be a -# number or a service name, and src_masklen and dst_masklen can -# be a number from 0 to 128. -# -# Default value: -# -# Unset -# -# If no port is specified, any port will match. If no -# src_masklen or src_masklen is given, the complete IP address -# has to match (i.e. 32 bits for IPv4 and 128 bits for IPv6). -# -# Effect if unset: -# -# Don't restrict access further than implied by listen-address -# -# Notes: -# -# Access controls are included at the request of ISPs and -# systems administrators, and are not usually needed by -# individual users. For a typical home user, it will normally -# suffice to ensure that Privoxy only listens on the localhost -# (127.0.0.1) or internal (home) network address by means of the -# listen-address option. -# -# Please see the warnings in the FAQ that Privoxy is not -# intended to be a substitute for a firewall or to encourage -# anyone to defer addressing basic security weaknesses. -# -# Multiple ACL lines are OK. If any ACLs are specified, Privoxy -# only talks to IP addresses that match at least one -# permit-access line and don't match any subsequent deny-access -# line. In other words, the last match wins, with the default -# being deny-access. -# -# If Privoxy is using a forwarder (see forward below) for a -# particular destination URL, the dst_addr that is examined is -# the address of the forwarder and NOT the address of the -# ultimate target. This is necessary because it may be -# impossible for the local Privoxy to determine the IP address -# of the ultimate target (that's often what gateways are used -# for). -# -# You should prefer using IP addresses over DNS names, because -# the address lookups take time. All DNS names must resolve! You -# can not use domain patterns like "*.org" or partial domain -# names. If a DNS name resolves to multiple IP addresses, only -# the first one is used. -# -# Some systems allow IPv4 clients to connect to IPv6 server -# sockets. Then the client's IPv4 address will be translated by -# the system into IPv6 address space with special prefix -# ::ffff:0:0/96 (so called IPv4 mapped IPv6 address). Privoxy -# can handle it and maps such ACL addresses automatically. -# -# Denying access to particular sites by ACL may have undesired -# side effects if the site in question is hosted on a machine -# which also hosts other sites (most sites are). -# -# Examples: -# -# Explicitly define the default behavior if no ACL and -# listen-address are set: "localhost" is OK. The absence of a -# dst_addr implies that all destination addresses are OK: -# -# permit-access localhost -# -# Allow any host on the same class C subnet as www.privoxy.org -# access to nothing but www.example.com (or other domains hosted -# on the same system): -# -# permit-access www.privoxy.org/24 www.example.com/32 -# -# Allow access from any host on the 26-bit subnet 192.168.45.64 -# to anywhere, with the exception that 192.168.45.73 may not -# access the IP address behind www.dirty-stuff.example.com: -# -# permit-access 192.168.45.64/26 -# deny-access 192.168.45.73 www.dirty-stuff.example.com -# -# Allow access from the IPv4 network 192.0.2.0/24 even if -# listening on an IPv6 wild card address (not supported on all -# platforms): -# -# permit-access 192.0.2.0/24 -# -# This is equivalent to the following line even if listening on -# an IPv4 address (not supported on all platforms): -# -# permit-access [::ffff:192.0.2.0]/120 -# -# -# 4.8. buffer-limit -# ================== -# -# Specifies: -# -# Maximum size of the buffer for content filtering. -# -# Type of value: -# -# Size in Kbytes -# -# Default value: -# -# 4096 -# -# Effect if unset: -# -# Use a 4MB (4096 KB) limit. -# -# Notes: -# -# For content filtering, i.e. the +filter and +deanimate-gif -# actions, it is necessary that Privoxy buffers the entire -# document body. This can be potentially dangerous, since a -# server could just keep sending data indefinitely and wait for -# your RAM to exhaust -- with nasty consequences. Hence this -# option. -# -# When a document buffer size reaches the buffer-limit, it is -# flushed to the client unfiltered and no further attempt to -# filter the rest of the document is made. Remember that there -# may be multiple threads running, which might require up to -# buffer-limit Kbytes each, unless you have enabled -# "single-threaded" above. -# -buffer-limit 4096 -# -# 4.9. enable-proxy-authentication-forwarding -# ============================================ -# -# Specifies: -# -# Whether or not proxy authentication through Privoxy should -# work. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Proxy authentication headers are removed. -# -# Notes: -# -# Privoxy itself does not support proxy authentication, but can -# allow clients to authenticate against Privoxy's parent proxy. -# -# By default Privoxy (3.0.21 and later) don't do that and remove -# Proxy-Authorization headers in requests and Proxy-Authenticate -# headers in responses to make it harder for malicious sites to -# trick inexperienced users into providing login information. -# -# If this option is enabled the headers are forwarded. -# -# Enabling this option is not recommended if there is no parent -# proxy that requires authentication or if the local network -# between Privoxy and the parent proxy isn't trustworthy. If -# proxy authentication is only required for some requests, it is -# recommended to use a client header filter to remove the -# authentication headers for requests where they aren't needed. -# -enable-proxy-authentication-forwarding 0 -# -# 5. FORWARDING -# ============== -# -# This feature allows routing of HTTP requests through a chain of -# multiple proxies. -# -# Forwarding can be used to chain Privoxy with a caching proxy to -# speed up browsing. Using a parent proxy may also be necessary if -# the machine that Privoxy runs on has no direct Internet access. -# -# Note that parent proxies can severely decrease your privacy level. -# For example a parent proxy could add your IP address to the -# request headers and if it's a caching proxy it may add the "Etag" -# header to revalidation requests again, even though you configured -# Privoxy to remove it. It may also ignore Privoxy's header time -# randomization and use the original values which could be used by -# the server as cookie replacement to track your steps between -# visits. -# -# Also specified here are SOCKS proxies. Privoxy supports the SOCKS -# 4 and SOCKS 4A protocols. -# -# -# 5.1. forward -# ============= -# -# Specifies: -# -# To which parent HTTP proxy specific requests should be routed. -# -# Type of value: -# -# target_pattern http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent[:port] is the DNS name or IP -# address of the parent HTTP proxy through which the requests -# should be forwarded, optionally followed by its listening port -# (default: 8000). Use a single dot (.) to denote "no -# forwarding". -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use parent HTTP proxies. -# -# Notes: -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made directly to the web servers. -# -# http_parent can be a numerical IPv6 address (if RFC 3493 is -# implemented). To prevent clashes with the port delimiter, the -# whole IP address has to be put into brackets. On the other -# hand a target_pattern containing an IPv6 address has to be put -# into angle brackets (normal brackets are reserved for regular -# expressions already). -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# Examples: -# -# Everything goes to an example parent proxy, except SSL on port -# 443 (which it doesn't handle): -# -# forward / parent-proxy.example.org:8080 -# forward :443 . -# -# Everything goes to our example ISP's caching proxy, except for -# requests to that ISP's sites: -# -# forward / caching-proxy.isp.example.net:8000 -# forward .isp.example.net . -# -# Parent proxy specified by an IPv6 address: -# -# forward / [2001:DB8::1]:8000 -# -# Suppose your parent proxy doesn't support IPv6: -# -# forward / parent-proxy.example.org:8000 -# forward ipv6-server.example.org . -# forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . -forward / 172.16.0.1:8080 -forward :443 . -# -# -# 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t -# ========================================================================= -# -# Specifies: -# -# Through which SOCKS proxy (and optionally to which parent HTTP -# proxy) specific requests should be routed. -# -# Type of value: -# -# target_pattern socks_proxy[:port] http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent and socks_proxy are IP -# addresses in dotted decimal notation or valid DNS names ( -# http_parent may be "." to denote "no HTTP forwarding"), and -# the optional port parameters are TCP ports, i.e. integer -# values from 1 to 65535 -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use SOCKS proxies. -# -# Notes: -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# The difference between forward-socks4 and forward-socks4a is -# that in the SOCKS 4A protocol, the DNS resolution of the -# target hostname happens on the SOCKS server, while in SOCKS 4 -# it happens locally. -# -# With forward-socks5 the DNS resolution will happen on the -# remote server as well. -# -# forward-socks5t works like vanilla forward-socks5 but lets -# Privoxy additionally use Tor-specific SOCKS extensions. -# Currently the only supported SOCKS extension is optimistic -# data which can reduce the latency for the first request made -# on a newly created connection. -# -# socks_proxy and http_parent can be a numerical IPv6 address -# (if RFC 3493 is implemented). To prevent clashes with the port -# delimiter, the whole IP address has to be put into brackets. -# On the other hand a target_pattern containing an IPv6 address -# has to be put into angle brackets (normal brackets are -# reserved for regular expressions already). -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made (HTTP-wise) directly to the -# web servers, albeit through a SOCKS proxy. -# -# Examples: -# -# From the company example.com, direct connections are made to -# all "internal" domains, but everything outbound goes through -# their ISP's proxy by way of example.com's corporate SOCKS 4A -# gateway to the Internet. -# -# forward-socks4a / socks-gw.example.com:1080 www-cache.isp.example.net:8080 -# forward .example.com . -# -# A rule that uses a SOCKS 4 gateway for all destinations but no -# HTTP parent looks like this: -# -# forward-socks4 / socks-gw.example.com:1080 . -# -# To chain Privoxy and Tor, both running on the same system, you -# would use something like: -# -# forward-socks5t / 127.0.0.1:9050 . -# -# Note that if you got Tor through one of the bundles, you may -# have to change the port from 9050 to 9150 (or even another -# one). For details, please check the documentation on the Tor -# website. -# -# The public Tor network can't be used to reach your local -# network, if you need to access local servers you therefore -# might want to make some exceptions: -# -# forward 192.168.*.*/ . -# forward 10.*.*.*/ . -# forward 127.*.*.*/ . -# -# Unencrypted connections to systems in these address ranges -# will be as (un)secure as the local network is, but the -# alternative is that you can't reach the local network through -# Privoxy at all. Of course this may actually be desired and -# there is no reason to make these exceptions if you aren't sure -# you need them. -# -# If you also want to be able to reach servers in your local -# network by using their names, you will need additional -# exceptions that look like this: -# -# forward localhost/ . -# -# -# 5.3. forwarded-connect-retries -# =============================== -# -# Specifies: -# -# How often Privoxy retries if a forwarded connection request -# fails. -# -# Type of value: -# -# Number of retries. -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Connections forwarded through other proxies are treated like -# direct connections and no retry attempts are made. -# -# Notes: -# -# forwarded-connect-retries is mainly interesting for socks4a -# connections, where Privoxy can't detect why the connections -# failed. The connection might have failed because of a DNS -# timeout in which case a retry makes sense, but it might also -# have failed because the server doesn't exist or isn't -# reachable. In this case the retry will just delay the -# appearance of Privoxy's error message. -# -# Note that in the context of this option, "forwarded -# connections" includes all connections that Privoxy forwards -# through other proxies. This option is not limited to the HTTP -# CONNECT method. -# -# Only use this option, if you are getting lots of -# forwarding-related error messages that go away when you try -# again manually. Start with a small value and check Privoxy's -# logfile from time to time, to see how many retries are usually -# needed. -# -# Examples: -# -# forwarded-connect-retries 1 -# -forwarded-connect-retries 0 -# -# 6. MISCELLANEOUS -# ================= -# -# 6.1. accept-intercepted-requests -# ================================= -# -# Specifies: -# -# Whether intercepted requests should be treated as valid. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Only proxy requests are accepted, intercepted requests are -# treated as invalid. -# -# Notes: -# -# If you don't trust your clients and want to force them to use -# Privoxy, enable this option and configure your packet filter -# to redirect outgoing HTTP connections into Privoxy. -# -# Note that intercepting encrypted connections (HTTPS) isn't -# supported. -# -# Make sure that Privoxy's own requests aren't redirected as -# well. Additionally take care that Privoxy can't intentionally -# connect to itself, otherwise you could run into redirection -# loops if Privoxy's listening port is reachable by the outside -# or an attacker has access to the pages you visit. -# -# Examples: -# -# accept-intercepted-requests 1 -# -accept-intercepted-requests 0 -# -# 6.2. allow-cgi-request-crunching -# ================================= -# -# Specifies: -# -# Whether requests to Privoxy's CGI pages can be blocked or -# redirected. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores block and redirect actions for its CGI pages. -# -# Notes: -# -# By default Privoxy ignores block or redirect actions for its -# CGI pages. Intercepting these requests can be useful in -# multi-user setups to implement fine-grained access control, -# but it can also render the complete web interface useless and -# make debugging problems painful if done without care. -# -# Don't enable this option unless you're sure that you really -# need it. -# -# Examples: -# -# allow-cgi-request-crunching 1 -# -allow-cgi-request-crunching 0 -# -# 6.3. split-large-forms -# ======================= -# -# Specifies: -# -# Whether the CGI interface should stay compatible with broken -# HTTP clients. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The CGI form generate long GET URLs. -# -# Notes: -# -# Privoxy's CGI forms can lead to rather long URLs. This isn't a -# problem as far as the HTTP standard is concerned, but it can -# confuse clients with arbitrary URL length limitations. -# -# Enabling split-large-forms causes Privoxy to divide big forms -# into smaller ones to keep the URL length down. It makes -# editing a lot less convenient and you can no longer submit all -# changes at once, but at least it works around this browser -# bug. -# -# If you don't notice any editing problems, there is no reason -# to enable this option, but if one of the submit buttons -# appears to be broken, you should give it a try. -# -# Examples: -# -# split-large-forms 1 -# -split-large-forms 0 -# -# 6.4. keep-alive-timeout -# ======================== -# -# Specifies: -# -# Number of seconds after which an open connection will no -# longer be reused. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not kept alive. -# -# Notes: -# -# This option allows clients to keep the connection to Privoxy -# alive. If the server supports it, Privoxy will keep the -# connection to the server alive as well. Under certain -# circumstances this may result in speed-ups. -# -# By default, Privoxy will close the connection to the server if -# the client connection gets closed, or if the specified timeout -# has been reached without a new request coming in. This -# behaviour can be changed with the connection-sharing option. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Note that a timeout of five seconds as used in the default -# configuration file significantly decreases the number of -# connections that will be reused. The value is used because -# some browsers limit the number of connections they open to a -# single host and apply the same limit to proxies. This can -# result in a single website "grabbing" all the connections the -# browser allows, which means connections to other websites -# can't be opened until the connections currently in use time -# out. -# -# Several users have reported this as a Privoxy bug, so the -# default value has been reduced. Consider increasing it to 300 -# seconds or even more if you think your browser can handle it. -# If your browser appears to be hanging, it probably can't. -# -# Examples: -# -# keep-alive-timeout 300 -# -keep-alive-timeout 5 -# -# 6.5. tolerate-pipelining -# ========================= -# -# Specifies: -# -# Whether or not pipelined requests should be served. -# -# Type of value: -# -# 0 or 1. -# -# Default value: -# -# None -# -# Effect if unset: -# -# If Privoxy receives more than one request at once, it -# terminates the client connection after serving the first one. -# -# Notes: -# -# Privoxy currently doesn't pipeline outgoing requests, thus -# allowing pipelining on the client connection is not guaranteed -# to improve the performance. -# -# By default Privoxy tries to discourage clients from pipelining -# by discarding aggressively pipelined requests, which forces -# the client to resend them through a new connection. -# -# This option lets Privoxy tolerate pipelining. Whether or not -# that improves performance mainly depends on the client -# configuration. -# -# If you are seeing problems with pages not properly loading, -# disabling this option could work around the problem. -# -# Examples: -# -# tolerate-pipelining 1 -# -tolerate-pipelining 1 -# -# 6.6. default-server-timeout -# ============================ -# -# Specifies: -# -# Assumed server-side keep-alive timeout if not specified by the -# server. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections for which the server didn't specify the keep-alive -# timeout are not reused. -# -# Notes: -# -# Enabling this option significantly increases the number of -# connections that are reused, provided the keep-alive-timeout -# option is also enabled. -# -# While it also increases the number of connections problems -# when Privoxy tries to reuse a connection that already has been -# closed on the server side, or is closed while Privoxy is -# trying to reuse it, this should only be a problem if it -# happens for the first request sent by the client. If it -# happens for requests on reused client connections, Privoxy -# will simply close the connection and the client is supposed to -# retry the request without bothering the user. -# -# Enabling this option is therefore only recommended if the -# connection-sharing option is disabled. -# -# It is an error to specify a value larger than the -# keep-alive-timeout value. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Examples: -# -# default-server-timeout 60 -# -#default-server-timeout 60 -# -# 6.7. connection-sharing -# ======================== -# -# Specifies: -# -# Whether or not outgoing connections that have been kept alive -# should be shared between different incoming connections. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not shared. -# -# Notes: -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support, or if it's disabled. -# -# Notes: -# -# Note that reusing connections doesn't necessary cause -# speedups. There are also a few privacy implications you should -# be aware of. -# -# If this option is effective, outgoing connections are shared -# between clients (if there are more than one) and closing the -# browser that initiated the outgoing connection does no longer -# affect the connection between Privoxy and the server unless -# the client's request hasn't been completed yet. -# -# If the outgoing connection is idle, it will not be closed -# until either Privoxy's or the server's timeout is reached. -# While it's open, the server knows that the system running -# Privoxy is still there. -# -# If there are more than one client (maybe even belonging to -# multiple users), they will be able to reuse each others -# connections. This is potentially dangerous in case of -# authentication schemes like NTLM where only the connection is -# authenticated, instead of requiring authentication for each -# request. -# -# If there is only a single client, and if said client can keep -# connections alive on its own, enabling this option has next to -# no effect. If the client doesn't support connection -# keep-alive, enabling this option may make sense as it allows -# Privoxy to keep outgoing connections alive even if the client -# itself doesn't support it. -# -# You should also be aware that enabling this option increases -# the likelihood of getting the "No server or forwarder data" -# error message, especially if you are using a slow connection -# to the Internet. -# -# This option should only be used by experienced users who -# understand the risks and can weight them against the benefits. -# -# Examples: -# -# connection-sharing 1 -# -#connection-sharing 1 -# -# 6.8. socket-timeout -# ==================== -# -# Specifies: -# -# Number of seconds after which a socket times out if no data is -# received. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# A default value of 300 seconds is used. -# -# Notes: -# -# The default is quite high and you probably want to reduce it. -# If you aren't using an occasionally slow proxy like Tor, -# reducing it to a few seconds should be fine. -# -# Examples: -# -# socket-timeout 300 -# -socket-timeout 300 -# -# 6.9. max-client-connections -# ============================ -# -# Specifies: -# -# Maximum number of client connections that will be served. -# -# Type of value: -# -# Positive number. -# -# Default value: -# -# 128 -# -# Effect if unset: -# -# Connections are served until a resource limit is reached. -# -# Notes: -# -# Privoxy creates one thread (or process) for every incoming -# client connection that isn't rejected based on the access -# control settings. -# -# If the system is powerful enough, Privoxy can theoretically -# deal with several hundred (or thousand) connections at the -# same time, but some operating systems enforce resource limits -# by shutting down offending processes and their default limits -# may be below the ones Privoxy would require under heavy load. -# -# Configuring Privoxy to enforce a connection limit below the -# thread or process limit used by the operating system makes -# sure this doesn't happen. Simply increasing the operating -# system's limit would work too, but if Privoxy isn't the only -# application running on the system, you may actually want to -# limit the resources used by Privoxy. -# -# If Privoxy is only used by a single trusted user, limiting the -# number of client connections is probably unnecessary. If there -# are multiple possibly untrusted users you probably still want -# to additionally use a packet filter to limit the maximal -# number of incoming connections per client. Otherwise a -# malicious user could intentionally create a high number of -# connections to prevent other users from using Privoxy. -# -# Obviously using this option only makes sense if you choose a -# limit below the one enforced by the operating system. -# -# One most POSIX-compliant systems Privoxy can't properly deal -# with more than FD_SETSIZE file descriptors at the same time -# and has to reject connections if the limit is reached. This -# will likely change in a future version, but currently this -# limit can't be increased without recompiling Privoxy with a -# different FD_SETSIZE limit. -# -# Examples: -# -# max-client-connections 256 -# -#max-client-connections 256 -# -# 6.10. handle-as-empty-doc-returns-ok -# ===================================== -# -# Specifies: -# -# The status code Privoxy returns for pages blocked with -# +handle-as-empty-document. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy returns a status 403(forbidden) for all blocked pages. -# -# Effect if set: -# -# Privoxy returns a status 200(OK) for pages blocked with -# +handle-as-empty-document and a status 403(Forbidden) for all -# other blocked pages. -# -# Notes: -# -# This directive was added as a work-around for Firefox bug -# 492459: "Websites are no longer rendered if SSL requests for -# JavaScripts are blocked by a proxy." -# (https://bugzilla.mozilla.org/show_bug.cgi?id=492459), the bug -# has been fixed for quite some time, but this directive is also -# useful to make it harder for websites to detect whether or not -# resources are being blocked. -# -#handle-as-empty-doc-returns-ok 1 -# -# 6.11. enable-compression -# ========================= -# -# Specifies: -# -# Whether or not buffered content is compressed before delivery. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy does not compress buffered content. -# -# Effect if set: -# -# Privoxy compresses buffered content before delivering it to -# the client, provided the client supports it. -# -# Notes: -# -# This directive is only supported if Privoxy has been compiled -# with FEATURE_COMPRESSION, which should not to be confused with -# FEATURE_ZLIB. -# -# Compressing buffered content is mainly useful if Privoxy and -# the client are running on different systems. If they are -# running on the same system, enabling compression is likely to -# slow things down. If you didn't measure otherwise, you should -# assume that it does and keep this option disabled. -# -# Privoxy will not compress buffered content below a certain -# length. -# -#enable-compression 1 -# -# 6.12. compression-level -# ======================== -# -# Specifies: -# -# The compression level that is passed to the zlib library when -# compressing buffered content. -# -# Type of value: -# -# Positive number ranging from 0 to 9. -# -# Default value: -# -# 1 -# -# Notes: -# -# Compressing the data more takes usually longer than -# compressing it less or not compressing it at all. Which level -# is best depends on the connection between Privoxy and the -# client. If you can't be bothered to benchmark it for yourself, -# you should stick with the default and keep compression -# disabled. -# -# If compression is disabled, the compression level is -# irrelevant. -# -# Examples: -# -# # Best speed (compared to the other levels) -# compression-level 1 -# -# # Best compression -# compression-level 9 -# -# # No compression. Only useful for testing as the added header -# # slightly increases the amount of data that has to be sent. -# # If your benchmark shows that using this compression level -# # is superior to using no compression at all, the benchmark -# # is likely to be flawed. -# compression-level 0 -# -# -#compression-level 1 -# -# 6.13. client-header-order -# ========================== -# -# Specifies: -# -# The order in which client headers are sorted before forwarding -# them. -# -# Type of value: -# -# Client header names delimited by spaces or tabs -# -# Default value: -# -# None -# -# Notes: -# -# By default Privoxy leaves the client headers in the order they -# were sent by the client. Headers are modified in-place, new -# headers are added at the end of the already existing headers. -# -# The header order can be used to fingerprint client requests -# independently of other headers like the User-Agent. -# -# This directive allows to sort the headers differently to -# better mimic a different User-Agent. Client headers will be -# emitted in the order given, headers whose name isn't -# explicitly specified are added at the end. -# -# Note that sorting headers in an uncommon way will make -# fingerprinting actually easier. Encrypted headers are not -# affected by this directive. -# -#client-header-order Host \ -# Accept \ -# Accept-Language \ -# Accept-Encoding \ -# Proxy-Connection \ -# Referer \ -# Cookie \ -# DNT \ -# If-Modified-Since \ -# Cache-Control \ -# Content-Length \ -# Content-Type -# -# -# 7. WINDOWS GUI OPTIONS -# ======================= -# -# Privoxy has a number of options specific to the Windows GUI -# interface: -# -# -# -# If "activity-animation" is set to 1, the Privoxy icon will animate -# when "Privoxy" is active. To turn off, set to 0. -# -#activity-animation 1 -# -# -# -# If "log-messages" is set to 1, Privoxy copies log messages to the -# console window. The log detail depends on the debug directive. -# -#log-messages 1 -# -# -# -# If "log-buffer-size" is set to 1, the size of the log buffer, i.e. -# the amount of memory used for the log messages displayed in the -# console window, will be limited to "log-max-lines" (see below). -# -# Warning: Setting this to 0 will result in the buffer to grow -# infinitely and eat up all your memory! -# -#log-buffer-size 1 -# -# -# -# log-max-lines is the maximum number of lines held in the log -# buffer. See above. -# -#log-max-lines 200 -# -# -# -# If "log-highlight-messages" is set to 1, Privoxy will highlight -# portions of the log messages with a bold-faced font: -# -#log-highlight-messages 1 -# -# -# -# The font used in the console window: -# -#log-font-name Comic Sans MS -# -# -# -# Font size used in the console window: -# -#log-font-size 8 -# -# -# -# "show-on-task-bar" controls whether or not Privoxy will appear as -# a button on the Task bar when minimized: -# -#show-on-task-bar 0 -# -# -# -# If "close-button-minimizes" is set to 1, the Windows close button -# will minimize Privoxy instead of closing the program (close with -# the exit option on the File menu). -# -#close-button-minimizes 1 -# -# -# -# The "hide-console" option is specific to the MS-Win console -# version of Privoxy. If this option is used, Privoxy will -# disconnect from and hide the command console. -# -#hide-console -# -# -# diff --git a/roles/features/templates/usr.sbin.dnsmasq.j2 b/roles/features/templates/usr.sbin.dnsmasq.j2 deleted file mode 100644 index 9b2c34bd..00000000 --- a/roles/features/templates/usr.sbin.dnsmasq.j2 +++ /dev/null @@ -1,68 +0,0 @@ -# ------------------------------------------------------------------ -# -# Copyright (C) 2009 John Dong -# Copyright (C) 2010 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License published by the Free Software Foundation. -# -# ------------------------------------------------------------------ - -@{TFTP_DIR}=/var/tftp /srv/tftpboot - -#include - -/usr/sbin/dnsmasq { - #include - #include - #include - - capability net_bind_service, - capability setgid, - capability setuid, - capability dac_override, - capability net_admin, # for DHCP server - capability net_raw, # for DHCP server ping checks - network inet raw, - - signal (receive) peer=/usr/sbin/libvirtd, - ptrace (readby) peer=/usr/sbin/libvirtd, - - /etc/dnsmasq.conf r, - /etc/dnsmasq.d/ r, - /etc/dnsmasq.d/* r, - /etc/ethers r, - /etc/NetworkManager/dnsmasq.d/ r, - /etc/NetworkManager/dnsmasq.d/* r, - /etc/block.hosts r, - - /usr/sbin/dnsmasq mr, - - /{,var/}run/*dnsmasq*.pid w, - /{,var/}run/dnsmasq-forwarders.conf r, - /{,var/}run/dnsmasq/ r, - /{,var/}run/dnsmasq/* rw, - - /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage - - # for the read-only TFTP server - @{TFTP_DIR}/ r, - @{TFTP_DIR}/** r, - - # libvirt config, lease and hosts files for dnsmasq - /var/lib/libvirt/dnsmasq/ r, - /var/lib/libvirt/dnsmasq/* r, - /var/lib/libvirt/dnsmasq/*.leases rw, - - # libvirt pid files for dnsmasq - /{,var/}run/libvirt/network/ r, - /{,var/}run/libvirt/network/*.pid rw, - - # NetworkManager integration - /{,var/}run/nm-dns-dnsmasq.conf r, - /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, - /{,var/}run/NetworkManager/dnsmasq.conf r, - /{,var/}run/NetworkManager/dnsmasq.pid w, - -} diff --git a/roles/features/templates/usr.sbin.privoxy.j2 b/roles/features/templates/usr.sbin.privoxy.j2 deleted file mode 100644 index 5f8d9ddf..00000000 --- a/roles/features/templates/usr.sbin.privoxy.j2 +++ /dev/null @@ -1,15 +0,0 @@ -#include - -/usr/sbin/privoxy { - #include - #include - - capability setgid, - capability setuid, - - /etc/privoxy/* r, - /etc/privoxy/templates/* r, - /run/privoxy.pid w, - /var/log/privoxy/logfile w, - -} From 4f46cc221a6698b0defe4b00e6fe72c2fe9b087f Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Wed, 17 Aug 2016 23:26:21 +0300 Subject: [PATCH 037/769] Split the features role in two #49 --- .../templates/10-loopback-services.cfg.j2 | 9 + roles/dns_adblocking/handlers/main.yml | 8 + roles/dns_adblocking/tasks/main.yml | 59 + roles/dns_adblocking/templates/adblock.sh | 50 + .../dns_adblocking/templates/dnsmasq.conf.j2 | 669 ++++++ .../templates/usr.sbin.dnsmasq.j2 | 68 + roles/proxy/handlers/main.yml | 9 + roles/proxy/tasks/main.yml | 61 + roles/proxy/templates/000-default.conf.j2 | 11 + roles/proxy/templates/pagespeed.conf.j2 | 369 +++ roles/proxy/templates/ports.conf.j2 | 13 + roles/proxy/templates/privoxy_config.j2 | 2107 +++++++++++++++++ roles/proxy/templates/usr.sbin.privoxy.j2 | 15 + 13 files changed, 3448 insertions(+) create mode 100644 roles/common/templates/10-loopback-services.cfg.j2 create mode 100644 roles/dns_adblocking/handlers/main.yml create mode 100644 roles/dns_adblocking/tasks/main.yml create mode 100644 roles/dns_adblocking/templates/adblock.sh create mode 100644 roles/dns_adblocking/templates/dnsmasq.conf.j2 create mode 100644 roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 create mode 100644 roles/proxy/handlers/main.yml create mode 100644 roles/proxy/tasks/main.yml create mode 100644 roles/proxy/templates/000-default.conf.j2 create mode 100644 roles/proxy/templates/pagespeed.conf.j2 create mode 100644 roles/proxy/templates/ports.conf.j2 create mode 100644 roles/proxy/templates/privoxy_config.j2 create mode 100644 roles/proxy/templates/usr.sbin.privoxy.j2 diff --git a/roles/common/templates/10-loopback-services.cfg.j2 b/roles/common/templates/10-loopback-services.cfg.j2 new file mode 100644 index 00000000..c5c47e47 --- /dev/null +++ b/roles/common/templates/10-loopback-services.cfg.j2 @@ -0,0 +1,9 @@ +auto lo:100 +iface lo:100 inet static + address 172.16.0.1 + netmask 255.255.255.255 + +iface lo:100 inet6 static + address FCAA::1 + netmask 64 + autoconf 0 diff --git a/roles/dns_adblocking/handlers/main.yml b/roles/dns_adblocking/handlers/main.yml new file mode 100644 index 00000000..562427dc --- /dev/null +++ b/roles/dns_adblocking/handlers/main.yml @@ -0,0 +1,8 @@ +- name: restart dnsmasq + service: name=dnsmasq state=restarted + +- name: restart apparmor + service: name=apparmor state=restarted + +- name: save iptables + command: service netfilter-persistent save diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml new file mode 100644 index 00000000..fcc5589d --- /dev/null +++ b/roles/dns_adblocking/tasks/main.yml @@ -0,0 +1,59 @@ +- name: Gather Facts + setup: + +- name: Dnsmasq installed + apt: name=dnsmasq state=latest + +- name: Dnsmasq profile for apparmor configured + template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 + notify: + - restart dnsmasq + +- name: Enforce the dnsmasq AppArmor policy + shell: aa-enforce usr.sbin.dnsmasq + +- name: Dnsmasq configured + template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf + notify: + - restart dnsmasq + +- name: Adblock script created + template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 + +- name: Adblock script added to cron + cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" + +- name: Update adblock hosts + shell: > + /opt/adblock.sh + +- name: Forward all DNS requests to the local resolver + iptables: + table: nat + chain: PREROUTING + protocol: udp + destination_port: 53 + source: "{{ vpn_network }}" + jump: DNAT + to_destination: 172.16.0.1:53 + notify: + - save iptables + +- name: Forward all DNS requests to the local resolver + iptables: + table: nat + chain: PREROUTING + protocol: udp + destination_port: 53 + source: "{{ vpn_network_ipv6 }}" + jump: DNAT + to_destination: fcaa::1:53 + ip_version: ipv6 + notify: + - save iptables + +- name: Dnsmasq enabled and started + service: name=dnsmasq state=started enabled=yes + +- name: Dnsmasq disabled and stopped + service: name=dnsmasq state=stopped enabled=no diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh new file mode 100644 index 00000000..a6a88581 --- /dev/null +++ b/roles/dns_adblocking/templates/adblock.sh @@ -0,0 +1,50 @@ +#!/bin/sh +#Block ads, malware, etc. + +# Redirect endpoint +ENDPOINT_IP4="0.0.0.0" +ENDPOINT_IP6="::" +IPV6="Y" + +#Delete the old block.hosts to make room for the updates +rm -f /etc/block.hosts + +echo 'Downloading hosts lists...' +#Download and process the files needed to make the lists (enable/add more, if you want) +wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > /tmp/block.build.list +wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list +wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list +wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list + +#Add black list, if non-empty +if [ -s "/etc/black.list" ] +then + echo 'Adding blacklist...' + awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /etc/black.list >> /tmp/block.build.list +fi + +#Sort the download/black lists +awk '{sub(/\r$/,"");print $1,$2}' /tmp/block.build.list|sort -u > /tmp/block.build.before + +#Filter (if applicable) +if [ -s "/etc/white.list" ] +then + #Filter the blacklist, supressing whitelist matches + # This is relatively slow =-( + echo 'Filtering white list...' + egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /etc/block.hosts +else + cat /tmp/block.build.before > /etc/block.hosts +fi + +if [ "$IPV6" = "Y" ] +then + safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') + safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') + echo 'Adding ipv6 support...' + sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /etc/block.hosts +fi + +service dnsmasq restart + +exit 0 diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 new file mode 100644 index 00000000..d28cfac3 --- /dev/null +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -0,0 +1,669 @@ +# Configuration file for dnsmasq. +# +# Format is one option per line, legal options are the same +# as the long options legal on the command line. See +# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details. + +# Listen on this specific port instead of the standard DNS port +# (53). Setting this to zero completely disables DNS function, +# leaving only DHCP and/or TFTP. +#port=5353 + +# The following two options make you a better netizen, since they +# tell dnsmasq to filter out queries which the public DNS cannot +# answer, and which load the servers (especially the root servers) +# unnecessarily. If you have a dial-on-demand link they also stop +# these requests from bringing up the link unnecessarily. + +# Never forward plain names (without a dot or domain part) +#domain-needed +# Never forward addresses in the non-routed address spaces. +#bogus-priv + +# Uncomment these to enable DNSSEC validation and caching: +# (Requires dnsmasq to be built with DNSSEC option.) +#conf-file=%%PREFIX%%/share/dnsmasq/trust-anchors.conf +#dnssec + +# Replies which are not DNSSEC signed may be legitimate, because the domain +# is unsigned, or may be forgeries. Setting this option tells dnsmasq to +# check that an unsigned reply is OK, by finding a secure proof that a DS +# record somewhere between the root and the domain does not exist. +# The cost of setting this is that even queries in unsigned domains will need +# one or more extra DNS queries to verify. +#dnssec-check-unsigned + +# Uncomment this to filter useless windows-originated DNS requests +# which can trigger dial-on-demand links needlessly. +# Note that (amongst other things) this blocks all SRV requests, +# so don't use it if you use eg Kerberos, SIP, XMMP or Google-talk. +# This option only affects forwarding, SRV records originating for +# dnsmasq (via srv-host= lines) are not suppressed by it. +#filterwin2k + +# Change this line if you want dns to get its upstream servers from +# somewhere other that /etc/resolv.conf +#resolv-file= + +# By default, dnsmasq will send queries to any of the upstream +# servers it knows about and tries to favour servers to are known +# to be up. Uncommenting this forces dnsmasq to try each query +# with each server strictly in the order they appear in +# /etc/resolv.conf +#strict-order + +# If you don't want dnsmasq to read /etc/resolv.conf or any other +# file, getting its servers from this file instead (see below), then +# uncomment this. +#no-resolv + +# If you don't want dnsmasq to poll /etc/resolv.conf or other resolv +# files for changes and re-read them then uncomment this. +#no-poll + +# Add other name servers here, with domain specs if they are for +# non-public domains. +#server=/localnet/192.168.0.1 + +# Example of routing PTR queries to nameservers: this will send all +# address->name queries for 192.168.3/24 to nameserver 10.1.2.3 +#server=/3.168.192.in-addr.arpa/10.1.2.3 + +# Add local-only domains here, queries in these domains are answered +# from /etc/hosts or DHCP only. +#local=/localnet/ + +# Add domains which you want to force to an IP address here. +# The example below send any host in double-click.net to a local +# web-server. +#address=/double-click.net/127.0.0.1 + +# --address (and --server) work with IPv6 addresses too. +#address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 + +# Add the IPs of all queries to yahoo.com, google.com, and their +# subdomains to the vpn and search ipsets: +#ipset=/yahoo.com/google.com/vpn,search + +# You can control how dnsmasq talks to a server: this forces +# queries to 10.1.2.3 to be routed via eth1 +# server=10.1.2.3@eth1 +server=8.8.8.8 +server=8.8.4.4 + +# and this sets the source (ie local) address used to talk to +# 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that +# IP on the machine, obviously). +# server=10.1.2.3@192.168.1.1#55 + +# If you want dnsmasq to change uid and gid to something other +# than the default, edit the following lines. +user=nobody +group=nogroup + +# If you want dnsmasq to listen for DHCP and DNS requests only on +# specified interfaces (and the loopback) give the name of the +# interface (eg eth0) here. +# Repeat the line for more than one interface. +#interface=lo +# Or you can specify which interface _not_ to listen on +#except-interface= +# Or which to listen on by address (remember to include 127.0.0.1 if +# you use this.) +listen-address=172.16.0.1,127.0.0.1,FCAA::1 +# If you want dnsmasq to provide only DNS service on an interface, +# configure it as shown above, and then use the following line to +# disable DHCP and TFTP on it. +#no-dhcp-interface= + +# On systems which support it, dnsmasq binds the wildcard address, +# even when it is listening on only some interfaces. It then discards +# requests that it shouldn't reply to. This has the advantage of +# working even when interfaces come and go and change address. If you +# want dnsmasq to really bind only the interfaces it is listening on, +# uncomment this option. About the only time you may need this is when +# running another nameserver on the same machine. +bind-interfaces + +# If you don't want dnsmasq to read /etc/hosts, uncomment the +# following line. +#no-hosts +# or if you want it to read another file, as well as /etc/hosts, use +# this. +addn-hosts=/etc/block.hosts + +# Set this (and domain: see below) if you want to have a domain +# automatically added to simple names in a hosts-file. +#expand-hosts + +# Set the domain for dnsmasq. this is optional, but if it is set, it +# does the following things. +# 1) Allows DHCP hosts to have fully qualified domain names, as long +# as the domain part matches this setting. +# 2) Sets the "domain" DHCP option thereby potentially setting the +# domain of all systems configured by DHCP +# 3) Provides the domain part for "expand-hosts" +#domain=thekelleys.org.uk + +# Set a different domain for a particular subnet +#domain=wireless.thekelleys.org.uk,192.168.2.0/24 + +# Same idea, but range rather then subnet +#domain=reserved.thekelleys.org.uk,192.68.3.100,192.168.3.200 + +# Uncomment this to enable the integrated DHCP server, you need +# to supply the range of addresses available for lease and optionally +# a lease time. If you have more than one network, you will need to +# repeat this for each network on which you want to supply DHCP +# service. +#dhcp-range=192.168.0.50,192.168.0.150,12h + +# This is an example of a DHCP range where the netmask is given. This +# is needed for networks we reach the dnsmasq DHCP server via a relay +# agent. If you don't know what a DHCP relay agent is, you probably +# don't need to worry about this. +#dhcp-range=192.168.0.50,192.168.0.150,255.255.255.0,12h + +# This is an example of a DHCP range which sets a tag, so that +# some DHCP options may be set only for this network. +#dhcp-range=set:red,192.168.0.50,192.168.0.150 + +# Use this DHCP range only when the tag "green" is set. +#dhcp-range=tag:green,192.168.0.50,192.168.0.150,12h + +# Specify a subnet which can't be used for dynamic address allocation, +# is available for hosts with matching --dhcp-host lines. Note that +# dhcp-host declarations will be ignored unless there is a dhcp-range +# of some type for the subnet in question. +# In this case the netmask is implied (it comes from the network +# configuration on the machine running dnsmasq) it is possible to give +# an explicit netmask instead. +#dhcp-range=192.168.0.0,static + +# Enable DHCPv6. Note that the prefix-length does not need to be specified +# and defaults to 64 if missing/ +#dhcp-range=1234::2, 1234::500, 64, 12h + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +#dhcp-range=1234::, ra-only + +# Do Router Advertisements, BUT NOT DHCP for this subnet, also try and +# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack +# hosts. Use the DHCPv4 lease to derive the name, network segment and +# MAC address and assume that the host will also have an +# IPv6 address calculated using the SLAAC alogrithm. +#dhcp-range=1234::, ra-names + +# Do Router Advertisements, BUT NOT DHCP for this subnet. +# Set the lifetime to 46 hours. (Note: minimum lifetime is 2 hours.) +#dhcp-range=1234::, ra-only, 48h + +# Do DHCP and Router Advertisements for this subnet. Set the A bit in the RA +# so that clients can use SLAAC addresses as well as DHCP ones. +#dhcp-range=1234::2, 1234::500, slaac + +# Do Router Advertisements and stateless DHCP for this subnet. Clients will +# not get addresses from DHCP, but they will get other configuration information. +# They will use SLAAC for addresses. +#dhcp-range=1234::, ra-stateless + +# Do stateless DHCP, SLAAC, and generate DNS names for SLAAC addresses +# from DHCPv4 leases. +#dhcp-range=1234::, ra-stateless, ra-names + +# Do router advertisements for all subnets where we're doing DHCPv6 +# Unless overriden by ra-stateless, ra-names, et al, the router +# advertisements will have the M and O bits set, so that the clients +# get addresses and configuration from DHCPv6, and the A bit reset, so the +# clients don't use SLAAC addresses. +#enable-ra + +# Supply parameters for specified hosts using DHCP. There are lots +# of valid alternatives, so we will give examples of each. Note that +# IP addresses DO NOT have to be in the range given above, they just +# need to be on the same network. The order of the parameters in these +# do not matter, it's permissible to give name, address and MAC in any +# order. + +# Always allocate the host with Ethernet address 11:22:33:44:55:66 +# The IP address 192.168.0.60 +#dhcp-host=11:22:33:44:55:66,192.168.0.60 + +# Always set the name of the host with hardware address +# 11:22:33:44:55:66 to be "fred" +#dhcp-host=11:22:33:44:55:66,fred + +# Always give the host with Ethernet address 11:22:33:44:55:66 +# the name fred and IP address 192.168.0.60 and lease time 45 minutes +#dhcp-host=11:22:33:44:55:66,fred,192.168.0.60,45m + +# Give a host with Ethernet address 11:22:33:44:55:66 or +# 12:34:56:78:90:12 the IP address 192.168.0.60. Dnsmasq will assume +# that these two Ethernet interfaces will never be in use at the same +# time, and give the IP address to the second, even if it is already +# in use by the first. Useful for laptops with wired and wireless +# addresses. +#dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.60 + +# Give the machine which says its name is "bert" IP address +# 192.168.0.70 and an infinite lease +#dhcp-host=bert,192.168.0.70,infinite + +# Always give the host with client identifier 01:02:02:04 +# the IP address 192.168.0.60 +#dhcp-host=id:01:02:02:04,192.168.0.60 + +# Always give the Infiniband interface with hardware address +# 80:00:00:48:fe:80:00:00:00:00:00:00:f4:52:14:03:00:28:05:81 the +# ip address 192.168.0.61. The client id is derived from the prefix +# ff:00:00:00:00:00:02:00:00:02:c9:00 and the last 8 pairs of +# hex digits of the hardware address. +#dhcp-host=id:ff:00:00:00:00:00:02:00:00:02:c9:00:f4:52:14:03:00:28:05:81,192.168.0.61 + +# Always give the host with client identifier "marjorie" +# the IP address 192.168.0.60 +#dhcp-host=id:marjorie,192.168.0.60 + +# Enable the address given for "judge" in /etc/hosts +# to be given to a machine presenting the name "judge" when +# it asks for a DHCP lease. +#dhcp-host=judge + +# Never offer DHCP service to a machine whose Ethernet +# address is 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,ignore + +# Ignore any client-id presented by the machine with Ethernet +# address 11:22:33:44:55:66. This is useful to prevent a machine +# being treated differently when running under different OS's or +# between PXE boot and OS boot. +#dhcp-host=11:22:33:44:55:66,id:* + +# Send extra options which are tagged as "red" to +# the machine with Ethernet address 11:22:33:44:55:66 +#dhcp-host=11:22:33:44:55:66,set:red + +# Send extra options which are tagged as "red" to +# any machine with Ethernet address starting 11:22:33: +#dhcp-host=11:22:33:*:*:*,set:red + +# Give a fixed IPv6 address and name to client with +# DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 +# Note the MAC addresses CANNOT be used to identify DHCPv6 clients. +# Note also the they [] around the IPv6 address are obilgatory. +#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] + +# Ignore any clients which are not specified in dhcp-host lines +# or /etc/ethers. Equivalent to ISC "deny unknown-clients". +# This relies on the special "known" tag which is set when +# a host is matched. +#dhcp-ignore=tag:!known + +# Send extra options which are tagged as "red" to any machine whose +# DHCP vendorclass string includes the substring "Linux" +#dhcp-vendorclass=set:red,Linux + +# Send extra options which are tagged as "red" to any machine one +# of whose DHCP userclass strings includes the substring "accounts" +#dhcp-userclass=set:red,accounts + +# Send extra options which are tagged as "red" to any machine whose +# MAC address matches the pattern. +#dhcp-mac=set:red,00:60:8C:*:*:* + +# If this line is uncommented, dnsmasq will read /etc/ethers and act +# on the ethernet-address/IP pairs found there just as if they had +# been given as --dhcp-host options. Useful if you keep +# MAC-address/host mappings there for other purposes. +#read-ethers + +# Send options to hosts which ask for a DHCP lease. +# See RFC 2132 for details of available options. +# Common options can be given to dnsmasq by name: +# run "dnsmasq --help dhcp" to get a list. +# Note that all the common settings, such as netmask and +# broadcast address, DNS server and default route, are given +# sane defaults by dnsmasq. You very likely will not need +# any dhcp-options. If you use Windows clients and Samba, there +# are some options which are recommended, they are detailed at the +# end of this section. + +# Override the default route supplied by dnsmasq, which assumes the +# router is the same machine as the one running dnsmasq. +#dhcp-option=3,1.2.3.4 + +# Do the same thing, but using the option name +#dhcp-option=option:router,1.2.3.4 + +# Override the default route supplied by dnsmasq and send no default +# route at all. Note that this only works for the options sent by +# default (1, 3, 6, 12, 28) the same line will send a zero-length option +# for all other option numbers. +#dhcp-option=3 + +# Set the NTP time server addresses to 192.168.0.4 and 10.10.0.5 +#dhcp-option=option:ntp-server,192.168.0.4,10.10.0.5 + +# Send DHCPv6 option. Note [] around IPv6 addresses. +#dhcp-option=option6:dns-server,[1234::77],[1234::88] + +# Send DHCPv6 option for namservers as the machine running +# dnsmasq and another. +#dhcp-option=option6:dns-server,[::],[1234::88] + +# Ask client to poll for option changes every six hours. (RFC4242) +#dhcp-option=option6:information-refresh-time,6h + +# Set option 58 client renewal time (T1). Defaults to half of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T1:1m + +# Set option 59 rebinding time (T2). Defaults to 7/8 of the +# lease time if not specified. (RFC2132) +#dhcp-option=option:T2:2m + +# Set the NTP time server address to be the same machine as +# is running dnsmasq +#dhcp-option=42,0.0.0.0 + +# Set the NIS domain name to "welly" +#dhcp-option=40,welly + +# Set the default time-to-live to 50 +#dhcp-option=23,50 + +# Set the "all subnets are local" flag +#dhcp-option=27,1 + +# Send the etherboot magic flag and then etherboot options (a string). +#dhcp-option=128,e4:45:74:68:00:00 +#dhcp-option=129,NIC=eepro100 + +# Specify an option which will only be sent to the "red" network +# (see dhcp-range for the declaration of the "red" network) +# Note that the tag: part must precede the option: part. +#dhcp-option = tag:red, option:ntp-server, 192.168.1.1 + +# The following DHCP options set up dnsmasq in the same way as is specified +# for the ISC dhcpcd in +# http://www.samba.org/samba/ftp/docs/textdocs/DHCP-Server-Configuration.txt +# adapted for a typical dnsmasq installation where the host running +# dnsmasq is also the host running samba. +# you may want to uncomment some or all of them if you use +# Windows clients and Samba. +#dhcp-option=19,0 # option ip-forwarding off +#dhcp-option=44,0.0.0.0 # set netbios-over-TCP/IP nameserver(s) aka WINS server(s) +#dhcp-option=45,0.0.0.0 # netbios datagram distribution server +#dhcp-option=46,8 # netbios node type + +# Send an empty WPAD option. This may be REQUIRED to get windows 7 to behave. +#dhcp-option=252,"\n" + +# Send RFC-3397 DNS domain search DHCP option. WARNING: Your DHCP client +# probably doesn't support this...... +#dhcp-option=option:domain-search,eng.apple.com,marketing.apple.com + +# Send RFC-3442 classless static routes (note the netmask encoding) +#dhcp-option=121,192.168.1.0/24,1.2.3.4,10.0.0.0/8,5.6.7.8 + +# Send vendor-class specific options encapsulated in DHCP option 43. +# The meaning of the options is defined by the vendor-class so +# options are sent only when the client supplied vendor class +# matches the class given here. (A substring match is OK, so "MSFT" +# matches "MSFT" and "MSFT 5.0"). This example sets the +# mtftp address to 0.0.0.0 for PXEClients. +#dhcp-option=vendor:PXEClient,1,0.0.0.0 + +# Send microsoft-specific option to tell windows to release the DHCP lease +# when it shuts down. Note the "i" flag, to tell dnsmasq to send the +# value as a four-byte integer - that's what microsoft wants. See +# http://technet2.microsoft.com/WindowsServer/en/library/a70f1bb7-d2d4-49f0-96d6-4b7414ecfaae1033.mspx?mfr=true +#dhcp-option=vendor:MSFT,2,1i + +# Send the Encapsulated-vendor-class ID needed by some configurations of +# Etherboot to allow is to recognise the DHCP server. +#dhcp-option=vendor:Etherboot,60,"Etherboot" + +# Send options to PXELinux. Note that we need to send the options even +# though they don't appear in the parameter request list, so we need +# to use dhcp-option-force here. +# See http://syslinux.zytor.com/pxe.php#special for details. +# Magic number - needed before anything else is recognised +#dhcp-option-force=208,f1:00:74:7e +# Configuration file name +#dhcp-option-force=209,configs/common +# Path prefix +#dhcp-option-force=210,/tftpboot/pxelinux/files/ +# Reboot time. (Note 'i' to send 32-bit value) +#dhcp-option-force=211,30i + +# Set the boot filename for netboot/PXE. You will only need +# this is you want to boot machines over the network and you will need +# a TFTP server; either dnsmasq's built in TFTP server or an +# external one. (See below for how to enable the TFTP server.) +#dhcp-boot=pxelinux.0 + +# The same as above, but use custom tftp-server instead machine running dnsmasq +#dhcp-boot=pxelinux,server.name,192.168.1.100 + +# Boot for Etherboot gPXE. The idea is to send two different +# filenames, the first loads gPXE, and the second tells gPXE what to +# load. The dhcp-match sets the gpxe tag for requests from gPXE. +#dhcp-match=set:gpxe,175 # gPXE sends a 175 option. +#dhcp-boot=tag:!gpxe,undionly.kpxe +#dhcp-boot=mybootimage + +# Encapsulated options for Etherboot gPXE. All the options are +# encapsulated within option 175 +#dhcp-option=encap:175, 1, 5b # priority code +#dhcp-option=encap:175, 176, 1b # no-proxydhcp +#dhcp-option=encap:175, 177, string # bus-id +#dhcp-option=encap:175, 189, 1b # BIOS drive code +#dhcp-option=encap:175, 190, user # iSCSI username +#dhcp-option=encap:175, 191, pass # iSCSI password + +# Test for the architecture of a netboot client. PXE clients are +# supposed to send their architecture as option 93. (See RFC 4578) +#dhcp-match=peecees, option:client-arch, 0 #x86-32 +#dhcp-match=itanics, option:client-arch, 2 #IA64 +#dhcp-match=hammers, option:client-arch, 6 #x86-64 +#dhcp-match=mactels, option:client-arch, 7 #EFI x86-64 + +# Do real PXE, rather than just booting a single file, this is an +# alternative to dhcp-boot. +#pxe-prompt="What system shall I netboot?" +# or with timeout before first available action is taken: +#pxe-prompt="Press F8 for menu.", 60 + +# Available boot services. for PXE. +#pxe-service=x86PC, "Boot from local disk" + +# Loads /pxelinux.0 from dnsmasq TFTP server. +#pxe-service=x86PC, "Install Linux", pxelinux + +# Loads /pxelinux.0 from TFTP server at 1.2.3.4. +# Beware this fails on old PXE ROMS. +#pxe-service=x86PC, "Install Linux", pxelinux, 1.2.3.4 + +# Use bootserver on network, found my multicast or broadcast. +#pxe-service=x86PC, "Install windows from RIS server", 1 + +# Use bootserver at a known IP address. +#pxe-service=x86PC, "Install windows from RIS server", 1, 1.2.3.4 + +# If you have multicast-FTP available, +# information for that can be passed in a similar way using options 1 +# to 5. See page 19 of +# http://download.intel.com/design/archives/wfm/downloads/pxespec.pdf + + +# Enable dnsmasq's built-in TFTP server +#enable-tftp + +# Set the root directory for files available via FTP. +#tftp-root=/var/ftpd + +# Do not abort if the tftp-root is unavailable +#tftp-no-fail + +# Make the TFTP server more secure: with this set, only files owned by +# the user dnsmasq is running as will be send over the net. +#tftp-secure + +# This option stops dnsmasq from negotiating a larger blocksize for TFTP +# transfers. It will slow things down, but may rescue some broken TFTP +# clients. +#tftp-no-blocksize + +# Set the boot file name only when the "red" tag is set. +#dhcp-boot=tag:red,pxelinux.red-net + +# An example of dhcp-boot with an external TFTP server: the name and IP +# address of the server are given after the filename. +# Can fail with old PXE ROMS. Overridden by --pxe-service. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,192.168.0.3 + +# If there are multiple external tftp servers having a same name +# (using /etc/hosts) then that name can be specified as the +# tftp_servername (the third option to dhcp-boot) and in that +# case dnsmasq resolves this name and returns the resultant IP +# addresses in round robin fasion. This facility can be used to +# load balance the tftp load among a set of servers. +#dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name + +# Set the limit on DHCP leases, the default is 150 +#dhcp-lease-max=150 + +# The DHCP server needs somewhere on disk to keep its lease database. +# This defaults to a sane location, but if you want to change it, use +# the line below. +#dhcp-leasefile=/var/lib/misc/dnsmasq.leases + +# Set the DHCP server to authoritative mode. In this mode it will barge in +# and take over the lease for any client which broadcasts on the network, +# whether it has a record of the lease or not. This avoids long timeouts +# when a machine wakes up on a new network. DO NOT enable this if there's +# the slightest chance that you might end up accidentally configuring a DHCP +# server for your campus/company accidentally. The ISC server uses +# the same option, and this URL provides more information: +# http://www.isc.org/files/auth.html +#dhcp-authoritative + +# Run an executable when a DHCP lease is created or destroyed. +# The arguments sent to the script are "add" or "del", +# then the MAC address, the IP address and finally the hostname +# if there is one. +#dhcp-script=/bin/echo + +# Set the cachesize here. +#cache-size=150 + +# If you want to disable negative caching, uncomment this. +#no-negcache + +# Normally responses which come from /etc/hosts and the DHCP lease +# file have Time-To-Live set as zero, which conventionally means +# do not cache further. If you are happy to trade lower load on the +# server for potentially stale date, you can set a time-to-live (in +# seconds) here. +#local-ttl= + +# If you want dnsmasq to detect attempts by Verisign to send queries +# to unregistered .com and .net hosts to its sitefinder service and +# have dnsmasq instead return the correct NXDOMAIN response, uncomment +# this line. You can add similar lines to do the same for other +# registries which have implemented wildcard A records. +#bogus-nxdomain=64.94.110.11 + +# If you want to fix up DNS results from upstream servers, use the +# alias option. This only works for IPv4. +# This alias makes a result of 1.2.3.4 appear as 5.6.7.8 +#alias=1.2.3.4,5.6.7.8 +# and this maps 1.2.3.x to 5.6.7.x +#alias=1.2.3.0,5.6.7.0,255.255.255.0 +# and this maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40 +#alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0 + +# Change these lines if you want dnsmasq to serve MX records. + +# Return an MX record named "maildomain.com" with target +# servermachine.com and preference 50 +#mx-host=maildomain.com,servermachine.com,50 + +# Set the default target for MX records created using the localmx option. +#mx-target=servermachine.com + +# Return an MX record pointing to the mx-target for all local +# machines. +#localmx + +# Return an MX record pointing to itself for all local machines. +#selfmx + +# Change the following lines if you want dnsmasq to serve SRV +# records. These are useful if you want to serve ldap requests for +# Active Directory and other windows-originated DNS requests. +# See RFC 2782. +# You may add multiple srv-host lines. +# The fields are ,,,, +# If the domain part if missing from the name (so that is just has the +# service and protocol sections) then the domain given by the domain= +# config option is used. (Note that expand-hosts does not need to be +# set for this to work.) + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389 + +# A SRV record sending LDAP for the example.com domain to +# ldapserver.example.com port 389 (using domain=) +#domain=example.com +#srv-host=_ldap._tcp,ldapserver.example.com,389 + +# Two SRV records for LDAP, each with different priorities +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,1 +#srv-host=_ldap._tcp.example.com,ldapserver.example.com,389,2 + +# A SRV record indicating that there is no LDAP server for the domain +# example.com +#srv-host=_ldap._tcp.example.com + +# The following line shows how to make dnsmasq serve an arbitrary PTR +# record. This is useful for DNS-SD. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for PTR records.) +#ptr-record=_http._tcp.dns-sd-services,"New Employee Page._http._tcp.dns-sd-services" + +# Change the following lines to enable dnsmasq to serve TXT records. +# These are used for things like SPF and zeroconf. (Note that the +# domain-name expansion done for SRV records _does_not +# occur for TXT records.) + +#Example SPF. +#txt-record=example.com,"v=spf1 a -all" + +#Example zeroconf +#txt-record=_http._tcp.example.com,name=value,paper=A4 + +# Provide an alias for a "local" DNS name. Note that this _only_ works +# for targets which are names from DHCP or /etc/hosts. Give host +# "bert" another name, bertrand +#cname=bertand,bert + +# For debugging purposes, log each DNS query as it passes through +# dnsmasq. +#log-queries + +# Log lots of extra information about DHCP transactions. +#log-dhcp + +# Include another lot of configuration options. +#conf-file=/etc/dnsmasq.more.conf +#conf-dir=/etc/dnsmasq.d + +# Include all the files in a directory except those ending in .bak +#conf-dir=/etc/dnsmasq.d,.bak + +# Include all files in a directory which end in .conf +#conf-dir=/etc/dnsmasq.d/,*.conf +# diff --git a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 new file mode 100644 index 00000000..9b2c34bd --- /dev/null +++ b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 @@ -0,0 +1,68 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2009 John Dong +# Copyright (C) 2010 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +@{TFTP_DIR}=/var/tftp /srv/tftpboot + +#include + +/usr/sbin/dnsmasq { + #include + #include + #include + + capability net_bind_service, + capability setgid, + capability setuid, + capability dac_override, + capability net_admin, # for DHCP server + capability net_raw, # for DHCP server ping checks + network inet raw, + + signal (receive) peer=/usr/sbin/libvirtd, + ptrace (readby) peer=/usr/sbin/libvirtd, + + /etc/dnsmasq.conf r, + /etc/dnsmasq.d/ r, + /etc/dnsmasq.d/* r, + /etc/ethers r, + /etc/NetworkManager/dnsmasq.d/ r, + /etc/NetworkManager/dnsmasq.d/* r, + /etc/block.hosts r, + + /usr/sbin/dnsmasq mr, + + /{,var/}run/*dnsmasq*.pid w, + /{,var/}run/dnsmasq-forwarders.conf r, + /{,var/}run/dnsmasq/ r, + /{,var/}run/dnsmasq/* rw, + + /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage + + # for the read-only TFTP server + @{TFTP_DIR}/ r, + @{TFTP_DIR}/** r, + + # libvirt config, lease and hosts files for dnsmasq + /var/lib/libvirt/dnsmasq/ r, + /var/lib/libvirt/dnsmasq/* r, + /var/lib/libvirt/dnsmasq/*.leases rw, + + # libvirt pid files for dnsmasq + /{,var/}run/libvirt/network/ r, + /{,var/}run/libvirt/network/*.pid rw, + + # NetworkManager integration + /{,var/}run/nm-dns-dnsmasq.conf r, + /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, + /{,var/}run/NetworkManager/dnsmasq.conf r, + /{,var/}run/NetworkManager/dnsmasq.pid w, + +} diff --git a/roles/proxy/handlers/main.yml b/roles/proxy/handlers/main.yml new file mode 100644 index 00000000..269a0ff8 --- /dev/null +++ b/roles/proxy/handlers/main.yml @@ -0,0 +1,9 @@ +- name: restart privoxy + service: name=privoxy state=restarted + +- name: restart apparmor + service: name=apparmor state=restarted + +- name: restart apache2 + service: name=apache2 state=restarted + diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml new file mode 100644 index 00000000..ae062cf3 --- /dev/null +++ b/roles/proxy/tasks/main.yml @@ -0,0 +1,61 @@ +- name: Gather Facts + setup: + +- name: Privoxy installed + apt: name=privoxy state=latest + +- name: Privoxy configured + template: src=privoxy_config.j2 dest=/etc/privoxy/config + notify: + - restart privoxy + +- name: Privoxy profile for apparmor configured + template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 + notify: + - restart privoxy + +- name: Enforce the privoxy AppArmor policy + shell: aa-enforce usr.sbin.privoxy + +- name: Privoxy enabled and started + service: name=privoxy state=started enabled=yes + +# PageSpeed + +- name: Apache installed + apt: name=apache2 state=latest + +- name: PageSpeed installed for x86_64 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb + when: ansible_architecture == "x86_64" + +- name: PageSpeed installed for i386 + apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb + when: ansible_architecture != "x86_64" + +- name: PageSpeed configured + template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf + notify: + - restart apache2 + +- name: Modules enabled + apache2_module: state=present name="{{ item }}" + with_items: + - proxy_http + - pagespeed + - cache + - proxy_connect + - proxy_html + - rewrite + notify: + - restart apache2 + +- name: VirtualHost configured for the PageSpeed module + template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf + notify: + - restart apache2 + +- name: Apache ports configured + template: src=ports.conf.j2 dest=/etc/apache2/ports.conf + notify: + - restart apache2 diff --git a/roles/proxy/templates/000-default.conf.j2 b/roles/proxy/templates/000-default.conf.j2 new file mode 100644 index 00000000..7aa917b7 --- /dev/null +++ b/roles/proxy/templates/000-default.conf.j2 @@ -0,0 +1,11 @@ + + + Order deny,allow + Allow from all + + RewriteEngine On + RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] + ProxyPass / http://$1 + ProxyPassReverse / http://$1 + ProxyPreserveHost On + diff --git a/roles/proxy/templates/pagespeed.conf.j2 b/roles/proxy/templates/pagespeed.conf.j2 new file mode 100644 index 00000000..3b89b758 --- /dev/null +++ b/roles/proxy/templates/pagespeed.conf.j2 @@ -0,0 +1,369 @@ + + # Turn on mod_pagespeed. To completely disable mod_pagespeed, you + # can set this to "off". + ModPagespeed on + + # We want VHosts to inherit global configuration. + # If this is not included, they'll be independent (except for inherently + # global options), at least for backwards compatibility. + ModPagespeedInheritVHostConfig on + + # Direct Apache to send all HTML output to the mod_pagespeed + # output handler. + AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html + + # If you want mod_pagespeed process XHTML as well, please uncomment this + # line. + # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml + + # The ModPagespeedFileCachePath directory must exist and be writable + # by the apache user (as specified by the User directive). + ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" + + # LogDir is needed to store various logs, including the statistics log + # required for the console. + ModPagespeedLogDir "/var/log/pagespeed" + + # The locations of SSL Certificates is distribution-dependent. + ModPagespeedSslCertDirectory "/etc/ssl/certs" + + + # If you want, you can use one or more memcached servers as the store for + # the mod_pagespeed cache. + # ModPagespeedMemcachedServers localhost:11211 + + # A portion of the cache can be kept in memory only, to reduce load on disk + # (or memcached) from many small files. + # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 + + # Override the mod_pagespeed 'rewrite level'. The default level + # "CoreFilters" uses a set of rewrite filters that are generally + # safe for most web pages. Most sites should not need to change + # this value and can instead fine-tune the configuration using the + # ModPagespeedDisableFilters and ModPagespeedEnableFilters + # directives, below. Valid values for ModPagespeedRewriteLevel are + # PassThrough, CoreFilters and TestingCoreFilters. + # + ModPagespeedRewriteLevel CoreFilters + + ModPagespeedEnableFilters combine_heads + ModPagespeedEnableFilters combine_javascript + ModPagespeedEnableFilters convert_jpeg_to_webp + ModPagespeedEnableFilters convert_png_to_jpeg + ModPagespeedEnableFilters inline_preview_images + ModPagespeedEnableFilters make_google_analytics_async + ModPagespeedEnableFilters move_css_above_scripts + ModPagespeedEnableFilters move_css_to_head + ModPagespeedEnableFilters resize_mobile_images + ModPagespeedEnableFilters sprite_images + + ModPagespeedEnableFilters defer_iframe + ModPagespeedEnableFilters defer_javascript + ModPagespeedEnableFilters lazyload_images + + # Explicitly disables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, if one + # of the filters in the CoreFilters needs to be disabled for a + # site, that filter can be added to + # ModPagespeedDisableFilters. This directive contains a + # comma-separated list of filter names, and can be repeated. + # + # ModPagespeedDisableFilters rewrite_images + + # Explicitly enables specific filters. This is useful in + # conjuction with ModPagespeedRewriteLevel. For instance, filters + # not included in the CoreFilters may be enabled using this + # directive. This directive contains a comma-separated list of + # filter names, and can be repeated. + # + # ModPagespeedEnableFilters rewrite_javascript,rewrite_css + # ModPagespeedEnableFilters collapse_whitespace,elide_attributes + + # Explicitly forbids the enabling of specific filters using either query + # parameters or request headers. This is useful, for example, when we do + # not want the filter to run for performance or security reasons. This + # directive contains a comma-separated list of filter names, and can be + # repeated. + # + # ModPagespeedForbidFilters rewrite_images + + # How long mod_pagespeed will wait to return an optimized resource + # (per flush window) on first request before giving up and returning the + # original (unoptimized) resource. After this deadline is exceeded the + # original resource is returned and the optimization is pushed to the + # background to be completed for future requests. Increasing this value will + # increase page latency, but might reduce load time (for instance on a + # bandwidth-constrained link where it's worth waiting for image + # compression to complete). If the value is less than or equal to zero + # mod_pagespeed will wait indefinitely for the rewrite to complete before + # returning. + # + # ModPagespeedRewriteDeadlinePerFlushMs 10 + + # ModPagespeedDomain + # authorizes rewriting of JS, CSS, and Image files found in this + # domain. By default only resources with the same origin as the + # HTML file are rewritten. For example: + # + ModPagespeedDomain * + # + # This will allow resources found on http://cdn.myhost.com to be + # rewritten in addition to those in the same domain as the HTML. + # + # Other domain-related directives (like ModPagespeedMapRewriteDomain + # and ModPagespeedMapOriginDomain) can also authorize domains. + # + # Wildcards (* and ?) are allowed in the domain specification. Be + # careful when using them as if you rewrite domains that do not + # send you traffic, then the site receiving the traffic will not + # know how to serve the rewritten content. + + # If you use downstream caches such as varnish or proxy_cache for caching + # HTML, you can configure pagespeed to work with these caches correctly + # using the following directives. Note that the values for + # ModPagespeedDownstreamCachePurgeLocationPrefix and + # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here + # in order to force the webmaster to choose appropriate value for these. + # + # ModPagespeedDownstreamCachePurgeLocationPrefix + # ModPagespeedDownstreamCachePurgeMethod PURGE + # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 + # ModPagespeedDownstreamCacheRebeaconingKey + + # Other defaults (cache sizes and thresholds): + # + # ModPagespeedFileCacheSizeKb 102400 + # ModPagespeedFileCacheCleanIntervalMs 3600000 + # ModPagespeedLRUCacheKbPerProcess 1024 + # ModPagespeedLRUCacheByteLimit 16384 + # ModPagespeedCssFlattenMaxBytes 102400 + # ModPagespeedCssInlineMaxBytes 2048 + # ModPagespeedCssImageInlineMaxBytes 0 + # ModPagespeedImageInlineMaxBytes 3072 + # ModPagespeedJsInlineMaxBytes 2048 + # ModPagespeedCssOutlineMinBytes 3000 + # ModPagespeedJsOutlineMinBytes 3000 + # ModPagespeedMaxCombinedCssBytes -1 + # ModPagespeedMaxCombinedJsBytes 92160 + + # Limit the number of inodes in the file cache. Set to 0 for no limit. + # The default value if this paramater is not specified is 0 (no limit). + ModPagespeedFileCacheInodeLimit 500000 + + # Bound the number of images that can be rewritten at any one time; this + # avoids overloading the CPU. Set this to 0 to remove the bound. + # + # ModPagespeedImageMaxRewritesAtOnce 8 + + # You can also customize the number of threads per Apache process + # mod_pagespeed will use to do resource optimization. Plain + # "rewrite threads" are used to do short, latency-sensitive work, + # while "expensive rewrite threads" are used for actual optimization + # work that's more computationally expensive. If you live these unset, + # or use values <= 0 the defaults will be used, which is 1 for both + # values when using non-threaded MPMs (e.g. prefork) and 4 for both + # on threaded MPMs (e.g. worker and event). These settings can only + # be changed globally, and not per virtual host. + # + # ModPagespeedNumRewriteThreads 4 + # ModPagespeedNumExpensiveRewriteThreads 4 + + # Randomly drop rewrites (*) to increase the chance of optimizing + # frequently fetched resources and decrease the chance of optimizing + # infrequently fetched resources. This can reduce CPU load. The default + # value of this parameter is 0 (no drops). 90 means that a resourced + # fetched once has a 10% probability of being optimized while a resource + # that is fetched 50 times has a 99.65% probability of being optimized. + # + # (*) Currently only CSS files and images are randomly dropped. Images + # within CSS files are not randomly dropped. + # + # ModPagespeedRewriteRandomDropPercentage 90 + + # Many filters modify the URLs of resources in HTML files. This is typically + # harmless but pages whose Javascript expects to read or modify the original + # URLs may break. The following parameters prevent filters from modifying + # URLs of their respective types. + # + # ModPagespeedJsPreserveURLs on + # ModPagespeedImagePreserveURLs on + # ModPagespeedCssPreserveURLs on + + # When PreserveURLs is on, it is still possible to enable browser-specific + # optimizations (for example, webp images can be served to browsers that + # will accept them). They'll be served with Vary: Accept or Vary: + # User-Agent headers as appropriate. Note that this may require configuring + # reverse proxy caches such as varnish to handle these headers properly. + # + # ModPagespeedFilters in_place_optimize_for_browser + + # Internet Explorer has difficulty caching resources with Vary: headers. + # They will either be uncached (older IE) or require revalidation. See: + # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx + # As a result we serve them as Cache-Control: private instead by default. + # If you are using a reverse proxy or CDN configured to cache content with + # the Vary: Accept header you should turn this setting off. + # + # ModPagespeedPrivateNotVaryForIE on + + # Settings for image optimization: + # + # Lossy image recompression quality (0 to 100, -1 just strips metadata): + # ModPagespeedImageRecompressionQuality 85 + # + # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedJpegRecompressionQuality -1 + # ModPagespeedJpegRecompressionQualityForSmallScreens 70 + + ModPagespeedJpegRecompressionQuality 75 + + # + # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): + # ModPagespeedWebpRecompressionQuality 80 + # ModPagespeedWebpRecompressionQualityForSmallScreens 70 + # + # Timeout for conversions to WebP format, in + # milliseconds. Negative values mean no timeout is applied. The + # default value is -1: + # ModPagespeedWebpTimeoutMs 5000 + # + # Percent of original image size below which optimized images are retained: + # ModPagespeedImageLimitOptimizedPercent 100 + # + # Percent of original image area below which image resizing will be + # attempted: + # ModPagespeedImageLimitResizeAreaPercent 100 + + # Settings for inline preview images + # + # Setting this to n restricts preview images to the first n images found on + # the page. The default of -1 means preview images can appear anywhere on + # the page (if those images appear above the fold). + # ModPagespeedMaxInlinedPreviewImagesIndex -1 + + # Sets the minimum size in bytes of any image for which a low quality image + # is generated. + # ModPagespeedMinImageSizeLowResolutionBytes 3072 + + # The maximum URL size is generally limited to about 2k characters + # due to IE: See http://support.microsoft.com/kb/208427/EN-US. + # Apache servers by default impose a further limitation of about + # 250 characters per URL segment (text between slashes). + # mod_pagespeed circumvents this limitation, but if you employ + # proxy servers in your path you may need to re-impose it by + # overriding the setting here. The default setting is 1024 + # characters. + # + # ModPagespeedMaxSegmentLength 250 + + # Uncomment this if you want to prevent mod_pagespeed from combining files + # (e.g. CSS files) across paths + # + # ModPagespeedCombineAcrossPaths off + + # Renaming JavaScript URLs can sometimes break them. With this + # option enabled, mod_pagespeed uses a simple heuristic to decide + # not to rename JavaScript that it thinks is introspective. + # + # You can uncomment this to let mod_pagespeed rename all JS files. + # + # ModPagespeedAvoidRenamingIntrospectiveJavascript off + + # Certain common JavaScript libraries are available from Google, which acts + # as a CDN and allows you to benefit from browser caching if a new visitor + # to your site previously visited another site that makes use of the same + # libraries as you do. Enable the following filter to turn on this feature. + # + # ModPagespeedEnableFilters canonicalize_javascript_libraries + + # The following line configures a library that is recognized by + # canonicalize_javascript_libraries. This will have no effect unless you + # enable this filter (generally by uncommenting the last line in the + # previous stanza). The format is: + # ModPagespeedLibrary bytes md5 canonical_url + # Where bytes and md5 are with respect to the *minified* JS; use + # js_minify --print_size_and_hash to obtain this data. + # Note that we can register multiple hashes for the same canonical url; + # we do this if there are versions available that have already been minified + # with more sophisticated tools. + # + # Additional library configuration can be found in + # pagespeed_libraries.conf included in the distribution. You should add + # new entries here, though, so that file can be automatically upgraded. + # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js + + # Explicitly tell mod_pagespeed to load some resources from disk. + # This will speed up load time and update frequency. + # + # This should only be used for static resources which do not need + # specific headers set or other processing by Apache. + # + # Both URL and filesystem path should specify directories and + # filesystem path must be absolute (for now). + # + # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" + + + # Enables server-side instrumentation and statistics. If this rewriter is + # enabled, then each rewritten HTML page will have instrumentation javacript + # added that sends latency beacons to /mod_pagespeed_beacon. These + # statistics can be accessed at /mod_pagespeed_statistics. You must also + # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers + # below. + # + # ModPagespeedEnableFilters add_instrumentation + + # The add_instrumentation filter sends a beacon after the page onload + # handler is called. The user might navigate to a new URL before this. If + # you enable the following directive, the beacon is sent as part of an + # onbeforeunload handler, for pages where navigation happens before the + # onload event. + # + # ModPagespeedReportUnloadTime on + + # Uncomment the following line so that ModPagespeed will not cache or + # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. + # Note that ModPagespeed always respects Vary: headers on html content. + # ModPagespeedRespectVary on + + # Uncomment the following line if you want to disable statistics entirely. + # + # ModPagespeedStatistics off + + # These handlers are central entry-points into the admin pages. + # By default, pagespeed_admin and pagespeed_global_admin present + # the same data, and differ only when + # ModPagespeedUsePerVHostStatistics is enabled. In that case, + # /pagespeed_global_admin sees aggregated data across all vhosts, + # and the /pagespeed_admin sees data only for a particular vhost. + # + # You may insert other "Allow from" lines to add hosts you want to + # allow to look at generated statistics. Another possibility is + # to comment out the "Order" and "Allow" options from the config + # file, to allow any client that can reach your server to access + # and change server state, such as statistics, caches, and + # messages. This might be appropriate in an experimental setup. + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_admin + + + Order allow,deny + Allow from localhost + Allow from 127.0.0.1 + SetHandler pagespeed_global_admin + + + # Enable logging of mod_pagespeed statistics, needed for the console. + ModPagespeedStatisticsLogging on + + # Page /mod_pagespeed_message lets you view the latest messages from + # mod_pagespeed, regardless of log-level in your httpd.conf + # ModPagespeedMessageBufferSize is the maximum number of bytes you would + # like to dump to your /mod_pagespeed_message page at one time, + # its default value is 100k bytes. + # Set it to 0 if you want to disable this feature. + ModPagespeedMessageBufferSize 100000 + diff --git a/roles/proxy/templates/ports.conf.j2 b/roles/proxy/templates/ports.conf.j2 new file mode 100644 index 00000000..2618436c --- /dev/null +++ b/roles/proxy/templates/ports.conf.j2 @@ -0,0 +1,13 @@ +# If you just change the port or add more ports here, you will likely also +# have to change the VirtualHost statement in +# /etc/apache2/sites-enabled/000-default.conf + +Listen 172.16.0.1:8080 + + + Listen 172.16.0.1:443 + + + + Listen 172.16.0.1:443 + diff --git a/roles/proxy/templates/privoxy_config.j2 b/roles/proxy/templates/privoxy_config.j2 new file mode 100644 index 00000000..dd55f0f3 --- /dev/null +++ b/roles/proxy/templates/privoxy_config.j2 @@ -0,0 +1,2107 @@ +# Sample Configuration File for Privoxy +# +# Id: config,v +# +# Copyright (C) 2001-2014 Privoxy Developers http://www.privoxy.org/ +# +#################################################################### +# # +# Table of Contents # +# # +# I. INTRODUCTION # +# II. FORMAT OF THE CONFIGURATION FILE # +# # +# 1. LOCAL SET-UP DOCUMENTATION # +# 2. CONFIGURATION AND LOG FILE LOCATIONS # +# 3. DEBUGGING # +# 4. ACCESS CONTROL AND SECURITY # +# 5. FORWARDING # +# 6. MISCELLANEOUS # +# 7. WINDOWS GUI OPTIONS # +# # +#################################################################### +# +# +# I. INTRODUCTION +# =============== +# +# This file holds Privoxy's main configuration. Privoxy detects +# configuration changes automatically, so you don't have to restart +# it unless you want to load a different configuration file. +# +# The configuration will be reloaded with the first request after +# the change was done, this request itself will still use the old +# configuration, though. In other words: it takes two requests +# before you see the result of your changes. Requests that are +# dropped due to ACL don't trigger reloads. +# +# When starting Privoxy on Unix systems, give the location of this +# file as last argument. On Windows systems, Privoxy will look for +# this file with the name 'config.txt' in the current working +# directory of the Privoxy process. +# +# +# II. FORMAT OF THE CONFIGURATION FILE +# ==================================== +# +# Configuration lines consist of an initial keyword followed by a +# list of values, all separated by whitespace (any number of spaces +# or tabs). For example, +# +# actionsfile default.action +# +# Indicates that the actionsfile is named 'default.action'. +# +# The '#' indicates a comment. Any part of a line following a '#' is +# ignored, except if the '#' is preceded by a '\'. +# +# Thus, by placing a # at the start of an existing configuration +# line, you can make it a comment and it will be treated as if it +# weren't there. This is called "commenting out" an option and can +# be useful. Removing the # again is called "uncommenting". +# +# Note that commenting out an option and leaving it at its default +# are two completely different things! Most options behave very +# differently when unset. See the "Effect if unset" explanation in +# each option's description for details. +# +# Long lines can be continued on the next line by using a `\' as the +# last character. +# +# +# 1. LOCAL SET-UP DOCUMENTATION +# ============================== +# +# If you intend to operate Privoxy for more users than just +# yourself, it might be a good idea to let them know how to reach +# you, what you block and why you do that, your policies, etc. +# +# +# 1.1. user-manual +# ================= +# +# Specifies: +# +# Location of the Privoxy User Manual. +# +# Type of value: +# +# A fully qualified URI +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# http://www.privoxy.org/version/user-manual/ will be used, +# where version is the Privoxy version. +# +# Notes: +# +# The User Manual URI is the single best source of information +# on Privoxy, and is used for help links from some of the +# internal CGI pages. The manual itself is normally packaged +# with the binary distributions, so you probably want to set +# this to a locally installed copy. +# +# Examples: +# +# The best all purpose solution is simply to put the full local +# PATH to where the User Manual is located: +# +# user-manual /usr/share/doc/privoxy/user-manual +# +# The User Manual is then available to anyone with access to +# Privoxy, by following the built-in URL: http:// +# config.privoxy.org/user-manual/ (or the shortcut: http://p.p/ +# user-manual/). +# +# If the documentation is not on the local system, it can be +# accessed from a remote server, as: +# +# user-manual http://example.com/privoxy/user-manual/ +# +# WARNING!!! +# +# If set, this option should be the first option in the +# config file, because it is used while the config file is +# being read. +# +user-manual /usr/share/doc/privoxy/user-manual +# +# 1.2. trust-info-url +# ==================== +# +# Specifies: +# +# A URL to be displayed in the error page that users will see if +# access to an untrusted page is denied. +# +# Type of value: +# +# URL +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No links are displayed on the "untrusted" error page. +# +# Notes: +# +# The value of this option only matters if the experimental +# trust mechanism has been activated. (See trustfile below.) +# +# If you use the trust mechanism, it is a good idea to write up +# some on-line documentation about your trust policy and to +# specify the URL(s) here. Use multiple times for multiple URLs. +# +# The URL(s) should be added to the trustfile as well, so users +# don't end up locked out from the information on why they were +# locked out in the first place! +# +#trust-info-url http://www.example.com/why_we_block.html +#trust-info-url http://www.example.com/what_we_allow.html +# +# 1.3. admin-address +# =================== +# +# Specifies: +# +# An email address to reach the Privoxy administrator. +# +# Type of value: +# +# Email address +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No email address is displayed on error pages and the CGI user +# interface. +# +# Notes: +# +# If both admin-address and proxy-info-url are unset, the whole +# "Local Privoxy Support" box on all generated pages will not be +# shown. +# +#admin-address privoxy-admin@example.com +# +# 1.4. proxy-info-url +# ==================== +# +# Specifies: +# +# A URL to documentation about the local Privoxy setup, +# configuration or policies. +# +# Type of value: +# +# URL +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# No link to local documentation is displayed on error pages and +# the CGI user interface. +# +# Notes: +# +# If both admin-address and proxy-info-url are unset, the whole +# "Local Privoxy Support" box on all generated pages will not be +# shown. +# +# This URL shouldn't be blocked ;-) +# +#proxy-info-url http://www.example.com/proxy-service.html +# +# 2. CONFIGURATION AND LOG FILE LOCATIONS +# ======================================== +# +# Privoxy can (and normally does) use a number of other files for +# additional configuration, help and logging. This section of the +# configuration file tells Privoxy where to find those other files. +# +# The user running Privoxy, must have read permission for all +# configuration files, and write permission to any files that would +# be modified, such as log files and actions files. +# +# +# 2.1. confdir +# ============= +# +# Specifies: +# +# The directory where the other configuration files are located. +# +# Type of value: +# +# Path name +# +# Default value: +# +# /etc/privoxy (Unix) or Privoxy installation dir (Windows) +# +# Effect if unset: +# +# Mandatory +# +# Notes: +# +# No trailing "/", please. +# +confdir /etc/privoxy +# +# 2.2. templdir +# ============== +# +# Specifies: +# +# An alternative directory where the templates are loaded from. +# +# Type of value: +# +# Path name +# +# Default value: +# +# unset +# +# Effect if unset: +# +# The templates are assumed to be located in confdir/template. +# +# Notes: +# +# Privoxy's original templates are usually overwritten with each +# update. Use this option to relocate customized templates that +# should be kept. As template variables might change between +# updates, you shouldn't expect templates to work with Privoxy +# releases other than the one they were part of, though. +# +#templdir . +# +# 2.3. temporary-directory +# ========================= +# +# Specifies: +# +# A directory where Privoxy can create temporary files. +# +# Type of value: +# +# Path name +# +# Default value: +# +# unset +# +# Effect if unset: +# +# No temporary files are created, external filters don't work. +# +# Notes: +# +# To execute external filters, Privoxy has to create temporary +# files. This directive specifies the directory the temporary +# files should be written to. +# +# It should be a directory only Privoxy (and trusted users) can +# access. +# +#temporary-directory . +# +# 2.4. logdir +# ============ +# +# Specifies: +# +# The directory where all logging takes place (i.e. where the +# logfile is located). +# +# Type of value: +# +# Path name +# +# Default value: +# +# /var/log/privoxy (Unix) or Privoxy installation dir (Windows) +# +# Effect if unset: +# +# Mandatory +# +# Notes: +# +# No trailing "/", please. +# +logdir /var/log/privoxy +# +# 2.5. actionsfile +# ================= +# +# Specifies: +# +# The actions file(s) to use +# +# Type of value: +# +# Complete file name, relative to confdir +# +# Default values: +# +# match-all.action # Actions that are applied to all sites and maybe overruled later on. +# +# default.action # Main actions file +# +# user.action # User customizations +# +# Effect if unset: +# +# No actions are taken at all. More or less neutral proxying. +# +# Notes: +# +# Multiple actionsfile lines are permitted, and are in fact +# recommended! +# +# The default values are default.action, which is the "main" +# actions file maintained by the developers, and user.action, +# where you can make your personal additions. +# +# Actions files contain all the per site and per URL +# configuration for ad blocking, cookie management, privacy +# considerations, etc. +# +actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on. +actionsfile default.action # Main actions file +actionsfile user.action # User customizations +# +# 2.6. filterfile +# ================ +# +# Specifies: +# +# The filter file(s) to use +# +# Type of value: +# +# File name, relative to confdir +# +# Default value: +# +# default.filter (Unix) or default.filter.txt (Windows) +# +# Effect if unset: +# +# No textual content filtering takes place, i.e. all +filter{name} +# actions in the actions files are turned neutral. +# +# Notes: +# +# Multiple filterfile lines are permitted. +# +# The filter files contain content modification rules that use +# regular expressions. These rules permit powerful changes on +# the content of Web pages, and optionally the headers as well, +# e.g., you could try to disable your favorite JavaScript +# annoyances, re-write the actual displayed text, or just have +# some fun playing buzzword bingo with web pages. +# +# The +filter{name} actions rely on the relevant filter (name) +# to be defined in a filter file! +# +# A pre-defined filter file called default.filter that contains +# a number of useful filters for common problems is included in +# the distribution. See the section on the filter action for a +# list. +# +# It is recommended to place any locally adapted filters into a +# separate file, such as user.filter. +# +filterfile default.filter +filterfile user.filter # User customizations +# +# 2.7. logfile +# ============= +# +# Specifies: +# +# The log file to use +# +# Type of value: +# +# File name, relative to logdir +# +# Default value: +# +# Unset (commented out). When activated: logfile (Unix) or +# privoxy.log (Windows). +# +# Effect if unset: +# +# No logfile is written. +# +# Notes: +# +# The logfile is where all logging and error messages are +# written. The level of detail and number of messages are set +# with the debug option (see below). The logfile can be useful +# for tracking down a problem with Privoxy (e.g., it's not +# blocking an ad you think it should block) and it can help you +# to monitor what your browser is doing. +# +# Depending on the debug options below, the logfile may be a +# privacy risk if third parties can get access to it. As most +# users will never look at it, Privoxy only logs fatal errors by +# default. +# +# For most troubleshooting purposes, you will have to change +# that, please refer to the debugging section for details. +# +# Any log files must be writable by whatever user Privoxy is +# being run as (on Unix, default user id is "privoxy"). +# +# To prevent the logfile from growing indefinitely, it is +# recommended to periodically rotate or shorten it. Many +# operating systems support log rotation out of the box, some +# require additional software to do it. For details, please +# refer to the documentation for your operating system. +# +logfile logfile +# +# 2.8. trustfile +# =============== +# +# Specifies: +# +# The name of the trust file to use +# +# Type of value: +# +# File name, relative to confdir +# +# Default value: +# +# Unset (commented out). When activated: trust (Unix) or +# trust.txt (Windows) +# +# Effect if unset: +# +# The entire trust mechanism is disabled. +# +# Notes: +# +# The trust mechanism is an experimental feature for building +# white-lists and should be used with care. It is NOT +# recommended for the casual user. +# +# If you specify a trust file, Privoxy will only allow access to +# sites that are specified in the trustfile. Sites can be listed +# in one of two ways: +# +# Prepending a ~ character limits access to this site only (and +# any sub-paths within this site), e.g. ~www.example.com allows +# access to ~www.example.com/features/news.html, etc. +# +# Or, you can designate sites as trusted referrers, by +# prepending the name with a + character. The effect is that +# access to untrusted sites will be granted -- but only if a +# link from this trusted referrer was used to get there. The +# link target will then be added to the "trustfile" so that +# future, direct accesses will be granted. Sites added via this +# mechanism do not become trusted referrers themselves (i.e. +# they are added with a ~ designation). There is a limit of 512 +# such entries, after which new entries will not be made. +# +# If you use the + operator in the trust file, it may grow +# considerably over time. +# +# It is recommended that Privoxy be compiled with the +# --disable-force, --disable-toggle and --disable-editor +# options, if this feature is to be used. +# +# Possible applications include limiting Internet access for +# children. +# +#trustfile trust +# +# 3. DEBUGGING +# ============= +# +# These options are mainly useful when tracing a problem. Note that +# you might also want to invoke Privoxy with the --no-daemon command +# line option when debugging. +# +# +# 3.1. debug +# =========== +# +# Specifies: +# +# Key values that determine what information gets logged. +# +# Type of value: +# +# Integer values +# +# Default value: +# +# 0 (i.e.: only fatal errors (that cause Privoxy to exit) are +# logged) +# +# Effect if unset: +# +# Default value is used (see above). +# +# Notes: +# +# The available debug levels are: +# +# debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. +# debug 2 # show each connection status +# debug 4 # show I/O status +# debug 8 # show header parsing +# debug 16 # log all data written to the network +# debug 32 # debug force feature +# debug 64 # debug regular expression filters +# debug 128 # debug redirects +# debug 256 # debug GIF de-animation +# debug 512 # Common Log Format +# debug 1024 # Log the destination for requests Privoxy didn't let through, and the reason why. +# debug 2048 # CGI user interface +# debug 4096 # Startup banner and warnings. +# debug 8192 # Non-fatal errors +# debug 32768 # log all data read from the network +# debug 65536 # Log the applying actions +# +# To select multiple debug levels, you can either add them or +# use multiple debug lines. +# +# A debug level of 1 is informative because it will show you +# each request as it happens. 1, 1024, 4096 and 8192 are +# recommended so that you will notice when things go wrong. The +# other levels are probably only of interest if you are hunting +# down a specific problem. They can produce a hell of an output +# (especially 16). +# +# If you are used to the more verbose settings, simply enable +# the debug lines below again. +# +# If you want to use pure CLF (Common Log Format), you should +# set "debug 512" ONLY and not enable anything else. +# +# Privoxy has a hard-coded limit for the length of log messages. +# If it's reached, messages are logged truncated and marked with +# "... [too long, truncated]". +# +# Please don't file any support requests without trying to +# reproduce the problem with increased debug level first. Once +# you read the log messages, you may even be able to solve the +# problem on your own. +# +#debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. +#debug 1024 # Actions that are applied to all sites and maybe overruled later on. +#debug 4096 # Startup banner and warnings +#debug 8192 # Non-fatal errors +# +# 3.2. single-threaded +# ===================== +# +# Specifies: +# +# Whether to run only one server thread. +# +# Type of value: +# +# 1 or 0 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Multi-threaded (or, where unavailable: forked) operation, i.e. +# the ability to serve multiple requests simultaneously. +# +# Notes: +# +# This option is only there for debugging purposes. It will +# drastically reduce performance. +# +#single-threaded 1 +# +# 3.3. hostname +# ============== +# +# Specifies: +# +# The hostname shown on the CGI pages. +# +# Type of value: +# +# Text +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# The hostname provided by the operating system is used. +# +# Notes: +# +# On some misconfigured systems resolving the hostname fails or +# takes too much time and slows Privoxy down. Setting a fixed +# hostname works around the problem. +# +# In other circumstances it might be desirable to show a +# hostname other than the one returned by the operating system. +# For example if the system has several different hostnames and +# you don't want to use the first one. +# +# Note that Privoxy does not validate the specified hostname +# value. +# +#hostname hostname.example.org +# +# 4. ACCESS CONTROL AND SECURITY +# =============================== +# +# This section of the config file controls the security-relevant +# aspects of Privoxy's configuration. +# +# +# 4.1. listen-address +# ==================== +# +# Specifies: +# +# The address and TCP port on which Privoxy will listen for +# client requests. +# +# Type of value: +# +# [IP-Address]:Port +# +# [Hostname]:Port +# +# Default value: +# +# 127.0.0.1:8118 +# +# Effect if unset: +# +# Bind to 127.0.0.1 (IPv4 localhost), port 8118. This is +# suitable and recommended for home users who run Privoxy on the +# same machine as their browser. +# +# Notes: +# +# You will need to configure your browser(s) to this proxy +# address and port. +# +# If you already have another service running on port 8118, or +# if you want to serve requests from other machines (e.g. on +# your local network) as well, you will need to override the +# default. +# +# You can use this statement multiple times to make Privoxy +# listen on more ports or more IP addresses. Suitable if your +# operating system does not support sharing IPv6 and IPv4 +# protocols on the same socket. +# +# If a hostname is used instead of an IP address, Privoxy will +# try to resolve it to an IP address and if there are multiple, +# use the first one returned. +# +# If the address for the hostname isn't already known on the +# system (for example because it's in /etc/hostname), this may +# result in DNS traffic. +# +# If the specified address isn't available on the system, or if +# the hostname can't be resolved, Privoxy will fail to start. +# +# IPv6 addresses containing colons have to be quoted by +# brackets. They can only be used if Privoxy has been compiled +# with IPv6 support. If you aren't sure if your version supports +# it, have a look at http://config.privoxy.org/show-status. +# +# Some operating systems will prefer IPv6 to IPv4 addresses even +# if the system has no IPv6 connectivity which is usually not +# expected by the user. Some even rely on DNS to resolve +# localhost which mean the "localhost" address used may not +# actually be local. +# +# It is therefore recommended to explicitly configure the +# intended IP address instead of relying on the operating +# system, unless there's a strong reason not to. +# +# If you leave out the address, Privoxy will bind to all IPv4 +# interfaces (addresses) on your machine and may become +# reachable from the Internet and/or the local network. Be aware +# that some GNU/Linux distributions modify that behaviour +# without updating the documentation. Check for non-standard +# patches if your Privoxy version behaves differently. +# +# If you configure Privoxy to be reachable from the network, +# consider using access control lists (ACL's, see below), and/or +# a firewall. +# +# If you open Privoxy to untrusted users, you will also want to +# make sure that the following actions are disabled: +# enable-edit-actions and enable-remote-toggle +# +# Example: +# +# Suppose you are running Privoxy on a machine which has the +# address 192.168.0.1 on your local private network +# (192.168.0.0) and has another outside connection with a +# different address. You want it to serve requests from inside +# only: +# +# listen-address 192.168.0.1:8118 +# +# Suppose you are running Privoxy on an IPv6-capable machine and +# you want it to listen on the IPv6 address of the loopback +# device: +# +# listen-address [::1]:8118 +# +# +listen-address 172.16.0.1:8118 +# +# 4.2. toggle +# ============ +# +# Specifies: +# +# Initial state of "toggle" status +# +# Type of value: +# +# 1 or 0 +# +# Default value: +# +# 1 +# +# Effect if unset: +# +# Act as if toggled on +# +# Notes: +# +# If set to 0, Privoxy will start in "toggled off" mode, i.e. +# mostly behave like a normal, content-neutral proxy with both +# ad blocking and content filtering disabled. See +# enable-remote-toggle below. +# +toggle 1 +# +# 4.3. enable-remote-toggle +# ========================== +# +# Specifies: +# +# Whether or not the web-based toggle feature may be used +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The web-based toggle feature is disabled. +# +# Notes: +# +# When toggled off, Privoxy mostly acts like a normal, +# content-neutral proxy, i.e. doesn't block ads or filter +# content. +# +# Access to the toggle feature can not be controlled separately +# by "ACLs" or HTTP authentication, so that everybody who can +# access Privoxy (see "ACLs" and listen-address above) can +# toggle it for all users. So this option is not recommended for +# multi-user environments with untrusted users. +# +# Note that malicious client side code (e.g Java) is also +# capable of using this option. +# +# As a lot of Privoxy users don't read documentation, this +# feature is disabled by default. +# +# Note that you must have compiled Privoxy with support for this +# feature, otherwise this option has no effect. +# +enable-remote-toggle 0 +# +# 4.4. enable-remote-http-toggle +# =============================== +# +# Specifies: +# +# Whether or not Privoxy recognizes special HTTP headers to +# change its behaviour. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy ignores special HTTP headers. +# +# Notes: +# +# When toggled on, the client can change Privoxy's behaviour by +# setting special HTTP headers. Currently the only supported +# special header is "X-Filter: No", to disable filtering for the +# ongoing request, even if it is enabled in one of the action +# files. +# +# This feature is disabled by default. If you are using Privoxy +# in a environment with trusted clients, you may enable this +# feature at your discretion. Note that malicious client side +# code (e.g Java) is also capable of using this feature. +# +# This option will be removed in future releases as it has been +# obsoleted by the more general header taggers. +# +enable-remote-http-toggle 0 +# +# 4.5. enable-edit-actions +# ========================= +# +# Specifies: +# +# Whether or not the web-based actions file editor may be used +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The web-based actions file editor is disabled. +# +# Notes: +# +# Access to the editor can not be controlled separately by +# "ACLs" or HTTP authentication, so that everybody who can +# access Privoxy (see "ACLs" and listen-address above) can +# modify its configuration for all users. +# +# This option is not recommended for environments with untrusted +# users and as a lot of Privoxy users don't read documentation, +# this feature is disabled by default. +# +# Note that malicious client side code (e.g Java) is also +# capable of using the actions editor and you shouldn't enable +# this options unless you understand the consequences and are +# sure your browser is configured correctly. +# +# Note that you must have compiled Privoxy with support for this +# feature, otherwise this option has no effect. +# +enable-edit-actions 0 +# +# 4.6. enforce-blocks +# ==================== +# +# Specifies: +# +# Whether the user is allowed to ignore blocks and can "go there +# anyway". +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Blocks are not enforced. +# +# Notes: +# +# Privoxy is mainly used to block and filter requests as a +# service to the user, for example to block ads and other junk +# that clogs the pipes. Privoxy's configuration isn't perfect +# and sometimes innocent pages are blocked. In this situation it +# makes sense to allow the user to enforce the request and have +# Privoxy ignore the block. +# +# In the default configuration Privoxy's "Blocked" page contains +# a "go there anyway" link to adds a special string (the force +# prefix) to the request URL. If that link is used, Privoxy will +# detect the force prefix, remove it again and let the request +# pass. +# +# Of course Privoxy can also be used to enforce a network +# policy. In that case the user obviously should not be able to +# bypass any blocks, and that's what the "enforce-blocks" option +# is for. If it's enabled, Privoxy hides the "go there anyway" +# link. If the user adds the force prefix by hand, it will not +# be accepted and the circumvention attempt is logged. +# +# Examples: +# +# enforce-blocks 1 +# +enforce-blocks 0 +# +# 4.7. ACLs: permit-access and deny-access +# ========================================= +# +# Specifies: +# +# Who can access what. +# +# Type of value: +# +# src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]] +# +# Where src_addr and dst_addr are IPv4 addresses in dotted +# decimal notation or valid DNS names, port is a port number, +# and src_masklen and dst_masklen are subnet masks in CIDR +# notation, i.e. integer values from 2 to 30 representing the +# length (in bits) of the network address. The masks and the +# whole destination part are optional. +# +# If your system implements RFC 3493, then src_addr and dst_addr +# can be IPv6 addresses delimeted by brackets, port can be a +# number or a service name, and src_masklen and dst_masklen can +# be a number from 0 to 128. +# +# Default value: +# +# Unset +# +# If no port is specified, any port will match. If no +# src_masklen or src_masklen is given, the complete IP address +# has to match (i.e. 32 bits for IPv4 and 128 bits for IPv6). +# +# Effect if unset: +# +# Don't restrict access further than implied by listen-address +# +# Notes: +# +# Access controls are included at the request of ISPs and +# systems administrators, and are not usually needed by +# individual users. For a typical home user, it will normally +# suffice to ensure that Privoxy only listens on the localhost +# (127.0.0.1) or internal (home) network address by means of the +# listen-address option. +# +# Please see the warnings in the FAQ that Privoxy is not +# intended to be a substitute for a firewall or to encourage +# anyone to defer addressing basic security weaknesses. +# +# Multiple ACL lines are OK. If any ACLs are specified, Privoxy +# only talks to IP addresses that match at least one +# permit-access line and don't match any subsequent deny-access +# line. In other words, the last match wins, with the default +# being deny-access. +# +# If Privoxy is using a forwarder (see forward below) for a +# particular destination URL, the dst_addr that is examined is +# the address of the forwarder and NOT the address of the +# ultimate target. This is necessary because it may be +# impossible for the local Privoxy to determine the IP address +# of the ultimate target (that's often what gateways are used +# for). +# +# You should prefer using IP addresses over DNS names, because +# the address lookups take time. All DNS names must resolve! You +# can not use domain patterns like "*.org" or partial domain +# names. If a DNS name resolves to multiple IP addresses, only +# the first one is used. +# +# Some systems allow IPv4 clients to connect to IPv6 server +# sockets. Then the client's IPv4 address will be translated by +# the system into IPv6 address space with special prefix +# ::ffff:0:0/96 (so called IPv4 mapped IPv6 address). Privoxy +# can handle it and maps such ACL addresses automatically. +# +# Denying access to particular sites by ACL may have undesired +# side effects if the site in question is hosted on a machine +# which also hosts other sites (most sites are). +# +# Examples: +# +# Explicitly define the default behavior if no ACL and +# listen-address are set: "localhost" is OK. The absence of a +# dst_addr implies that all destination addresses are OK: +# +# permit-access localhost +# +# Allow any host on the same class C subnet as www.privoxy.org +# access to nothing but www.example.com (or other domains hosted +# on the same system): +# +# permit-access www.privoxy.org/24 www.example.com/32 +# +# Allow access from any host on the 26-bit subnet 192.168.45.64 +# to anywhere, with the exception that 192.168.45.73 may not +# access the IP address behind www.dirty-stuff.example.com: +# +# permit-access 192.168.45.64/26 +# deny-access 192.168.45.73 www.dirty-stuff.example.com +# +# Allow access from the IPv4 network 192.0.2.0/24 even if +# listening on an IPv6 wild card address (not supported on all +# platforms): +# +# permit-access 192.0.2.0/24 +# +# This is equivalent to the following line even if listening on +# an IPv4 address (not supported on all platforms): +# +# permit-access [::ffff:192.0.2.0]/120 +# +# +# 4.8. buffer-limit +# ================== +# +# Specifies: +# +# Maximum size of the buffer for content filtering. +# +# Type of value: +# +# Size in Kbytes +# +# Default value: +# +# 4096 +# +# Effect if unset: +# +# Use a 4MB (4096 KB) limit. +# +# Notes: +# +# For content filtering, i.e. the +filter and +deanimate-gif +# actions, it is necessary that Privoxy buffers the entire +# document body. This can be potentially dangerous, since a +# server could just keep sending data indefinitely and wait for +# your RAM to exhaust -- with nasty consequences. Hence this +# option. +# +# When a document buffer size reaches the buffer-limit, it is +# flushed to the client unfiltered and no further attempt to +# filter the rest of the document is made. Remember that there +# may be multiple threads running, which might require up to +# buffer-limit Kbytes each, unless you have enabled +# "single-threaded" above. +# +buffer-limit 4096 +# +# 4.9. enable-proxy-authentication-forwarding +# ============================================ +# +# Specifies: +# +# Whether or not proxy authentication through Privoxy should +# work. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Proxy authentication headers are removed. +# +# Notes: +# +# Privoxy itself does not support proxy authentication, but can +# allow clients to authenticate against Privoxy's parent proxy. +# +# By default Privoxy (3.0.21 and later) don't do that and remove +# Proxy-Authorization headers in requests and Proxy-Authenticate +# headers in responses to make it harder for malicious sites to +# trick inexperienced users into providing login information. +# +# If this option is enabled the headers are forwarded. +# +# Enabling this option is not recommended if there is no parent +# proxy that requires authentication or if the local network +# between Privoxy and the parent proxy isn't trustworthy. If +# proxy authentication is only required for some requests, it is +# recommended to use a client header filter to remove the +# authentication headers for requests where they aren't needed. +# +enable-proxy-authentication-forwarding 0 +# +# 5. FORWARDING +# ============== +# +# This feature allows routing of HTTP requests through a chain of +# multiple proxies. +# +# Forwarding can be used to chain Privoxy with a caching proxy to +# speed up browsing. Using a parent proxy may also be necessary if +# the machine that Privoxy runs on has no direct Internet access. +# +# Note that parent proxies can severely decrease your privacy level. +# For example a parent proxy could add your IP address to the +# request headers and if it's a caching proxy it may add the "Etag" +# header to revalidation requests again, even though you configured +# Privoxy to remove it. It may also ignore Privoxy's header time +# randomization and use the original values which could be used by +# the server as cookie replacement to track your steps between +# visits. +# +# Also specified here are SOCKS proxies. Privoxy supports the SOCKS +# 4 and SOCKS 4A protocols. +# +# +# 5.1. forward +# ============= +# +# Specifies: +# +# To which parent HTTP proxy specific requests should be routed. +# +# Type of value: +# +# target_pattern http_parent[:port] +# +# where target_pattern is a URL pattern that specifies to which +# requests (i.e. URLs) this forward rule shall apply. Use / to +# denote "all URLs". http_parent[:port] is the DNS name or IP +# address of the parent HTTP proxy through which the requests +# should be forwarded, optionally followed by its listening port +# (default: 8000). Use a single dot (.) to denote "no +# forwarding". +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# Don't use parent HTTP proxies. +# +# Notes: +# +# If http_parent is ".", then requests are not forwarded to +# another HTTP proxy but are made directly to the web servers. +# +# http_parent can be a numerical IPv6 address (if RFC 3493 is +# implemented). To prevent clashes with the port delimiter, the +# whole IP address has to be put into brackets. On the other +# hand a target_pattern containing an IPv6 address has to be put +# into angle brackets (normal brackets are reserved for regular +# expressions already). +# +# Multiple lines are OK, they are checked in sequence, and the +# last match wins. +# +# Examples: +# +# Everything goes to an example parent proxy, except SSL on port +# 443 (which it doesn't handle): +# +# forward / parent-proxy.example.org:8080 +# forward :443 . +# +# Everything goes to our example ISP's caching proxy, except for +# requests to that ISP's sites: +# +# forward / caching-proxy.isp.example.net:8000 +# forward .isp.example.net . +# +# Parent proxy specified by an IPv6 address: +# +# forward / [2001:DB8::1]:8000 +# +# Suppose your parent proxy doesn't support IPv6: +# +# forward / parent-proxy.example.org:8000 +# forward ipv6-server.example.org . +# forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . +forward / 172.16.0.1:8080 +forward :443 . +# +# +# 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t +# ========================================================================= +# +# Specifies: +# +# Through which SOCKS proxy (and optionally to which parent HTTP +# proxy) specific requests should be routed. +# +# Type of value: +# +# target_pattern socks_proxy[:port] http_parent[:port] +# +# where target_pattern is a URL pattern that specifies to which +# requests (i.e. URLs) this forward rule shall apply. Use / to +# denote "all URLs". http_parent and socks_proxy are IP +# addresses in dotted decimal notation or valid DNS names ( +# http_parent may be "." to denote "no HTTP forwarding"), and +# the optional port parameters are TCP ports, i.e. integer +# values from 1 to 65535 +# +# Default value: +# +# Unset +# +# Effect if unset: +# +# Don't use SOCKS proxies. +# +# Notes: +# +# Multiple lines are OK, they are checked in sequence, and the +# last match wins. +# +# The difference between forward-socks4 and forward-socks4a is +# that in the SOCKS 4A protocol, the DNS resolution of the +# target hostname happens on the SOCKS server, while in SOCKS 4 +# it happens locally. +# +# With forward-socks5 the DNS resolution will happen on the +# remote server as well. +# +# forward-socks5t works like vanilla forward-socks5 but lets +# Privoxy additionally use Tor-specific SOCKS extensions. +# Currently the only supported SOCKS extension is optimistic +# data which can reduce the latency for the first request made +# on a newly created connection. +# +# socks_proxy and http_parent can be a numerical IPv6 address +# (if RFC 3493 is implemented). To prevent clashes with the port +# delimiter, the whole IP address has to be put into brackets. +# On the other hand a target_pattern containing an IPv6 address +# has to be put into angle brackets (normal brackets are +# reserved for regular expressions already). +# +# If http_parent is ".", then requests are not forwarded to +# another HTTP proxy but are made (HTTP-wise) directly to the +# web servers, albeit through a SOCKS proxy. +# +# Examples: +# +# From the company example.com, direct connections are made to +# all "internal" domains, but everything outbound goes through +# their ISP's proxy by way of example.com's corporate SOCKS 4A +# gateway to the Internet. +# +# forward-socks4a / socks-gw.example.com:1080 www-cache.isp.example.net:8080 +# forward .example.com . +# +# A rule that uses a SOCKS 4 gateway for all destinations but no +# HTTP parent looks like this: +# +# forward-socks4 / socks-gw.example.com:1080 . +# +# To chain Privoxy and Tor, both running on the same system, you +# would use something like: +# +# forward-socks5t / 127.0.0.1:9050 . +# +# Note that if you got Tor through one of the bundles, you may +# have to change the port from 9050 to 9150 (or even another +# one). For details, please check the documentation on the Tor +# website. +# +# The public Tor network can't be used to reach your local +# network, if you need to access local servers you therefore +# might want to make some exceptions: +# +# forward 192.168.*.*/ . +# forward 10.*.*.*/ . +# forward 127.*.*.*/ . +# +# Unencrypted connections to systems in these address ranges +# will be as (un)secure as the local network is, but the +# alternative is that you can't reach the local network through +# Privoxy at all. Of course this may actually be desired and +# there is no reason to make these exceptions if you aren't sure +# you need them. +# +# If you also want to be able to reach servers in your local +# network by using their names, you will need additional +# exceptions that look like this: +# +# forward localhost/ . +# +# +# 5.3. forwarded-connect-retries +# =============================== +# +# Specifies: +# +# How often Privoxy retries if a forwarded connection request +# fails. +# +# Type of value: +# +# Number of retries. +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Connections forwarded through other proxies are treated like +# direct connections and no retry attempts are made. +# +# Notes: +# +# forwarded-connect-retries is mainly interesting for socks4a +# connections, where Privoxy can't detect why the connections +# failed. The connection might have failed because of a DNS +# timeout in which case a retry makes sense, but it might also +# have failed because the server doesn't exist or isn't +# reachable. In this case the retry will just delay the +# appearance of Privoxy's error message. +# +# Note that in the context of this option, "forwarded +# connections" includes all connections that Privoxy forwards +# through other proxies. This option is not limited to the HTTP +# CONNECT method. +# +# Only use this option, if you are getting lots of +# forwarding-related error messages that go away when you try +# again manually. Start with a small value and check Privoxy's +# logfile from time to time, to see how many retries are usually +# needed. +# +# Examples: +# +# forwarded-connect-retries 1 +# +forwarded-connect-retries 0 +# +# 6. MISCELLANEOUS +# ================= +# +# 6.1. accept-intercepted-requests +# ================================= +# +# Specifies: +# +# Whether intercepted requests should be treated as valid. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Only proxy requests are accepted, intercepted requests are +# treated as invalid. +# +# Notes: +# +# If you don't trust your clients and want to force them to use +# Privoxy, enable this option and configure your packet filter +# to redirect outgoing HTTP connections into Privoxy. +# +# Note that intercepting encrypted connections (HTTPS) isn't +# supported. +# +# Make sure that Privoxy's own requests aren't redirected as +# well. Additionally take care that Privoxy can't intentionally +# connect to itself, otherwise you could run into redirection +# loops if Privoxy's listening port is reachable by the outside +# or an attacker has access to the pages you visit. +# +# Examples: +# +# accept-intercepted-requests 1 +# +accept-intercepted-requests 0 +# +# 6.2. allow-cgi-request-crunching +# ================================= +# +# Specifies: +# +# Whether requests to Privoxy's CGI pages can be blocked or +# redirected. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy ignores block and redirect actions for its CGI pages. +# +# Notes: +# +# By default Privoxy ignores block or redirect actions for its +# CGI pages. Intercepting these requests can be useful in +# multi-user setups to implement fine-grained access control, +# but it can also render the complete web interface useless and +# make debugging problems painful if done without care. +# +# Don't enable this option unless you're sure that you really +# need it. +# +# Examples: +# +# allow-cgi-request-crunching 1 +# +allow-cgi-request-crunching 0 +# +# 6.3. split-large-forms +# ======================= +# +# Specifies: +# +# Whether the CGI interface should stay compatible with broken +# HTTP clients. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# The CGI form generate long GET URLs. +# +# Notes: +# +# Privoxy's CGI forms can lead to rather long URLs. This isn't a +# problem as far as the HTTP standard is concerned, but it can +# confuse clients with arbitrary URL length limitations. +# +# Enabling split-large-forms causes Privoxy to divide big forms +# into smaller ones to keep the URL length down. It makes +# editing a lot less convenient and you can no longer submit all +# changes at once, but at least it works around this browser +# bug. +# +# If you don't notice any editing problems, there is no reason +# to enable this option, but if one of the submit buttons +# appears to be broken, you should give it a try. +# +# Examples: +# +# split-large-forms 1 +# +split-large-forms 0 +# +# 6.4. keep-alive-timeout +# ======================== +# +# Specifies: +# +# Number of seconds after which an open connection will no +# longer be reused. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections are not kept alive. +# +# Notes: +# +# This option allows clients to keep the connection to Privoxy +# alive. If the server supports it, Privoxy will keep the +# connection to the server alive as well. Under certain +# circumstances this may result in speed-ups. +# +# By default, Privoxy will close the connection to the server if +# the client connection gets closed, or if the specified timeout +# has been reached without a new request coming in. This +# behaviour can be changed with the connection-sharing option. +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support. +# +# Note that a timeout of five seconds as used in the default +# configuration file significantly decreases the number of +# connections that will be reused. The value is used because +# some browsers limit the number of connections they open to a +# single host and apply the same limit to proxies. This can +# result in a single website "grabbing" all the connections the +# browser allows, which means connections to other websites +# can't be opened until the connections currently in use time +# out. +# +# Several users have reported this as a Privoxy bug, so the +# default value has been reduced. Consider increasing it to 300 +# seconds or even more if you think your browser can handle it. +# If your browser appears to be hanging, it probably can't. +# +# Examples: +# +# keep-alive-timeout 300 +# +keep-alive-timeout 5 +# +# 6.5. tolerate-pipelining +# ========================= +# +# Specifies: +# +# Whether or not pipelined requests should be served. +# +# Type of value: +# +# 0 or 1. +# +# Default value: +# +# None +# +# Effect if unset: +# +# If Privoxy receives more than one request at once, it +# terminates the client connection after serving the first one. +# +# Notes: +# +# Privoxy currently doesn't pipeline outgoing requests, thus +# allowing pipelining on the client connection is not guaranteed +# to improve the performance. +# +# By default Privoxy tries to discourage clients from pipelining +# by discarding aggressively pipelined requests, which forces +# the client to resend them through a new connection. +# +# This option lets Privoxy tolerate pipelining. Whether or not +# that improves performance mainly depends on the client +# configuration. +# +# If you are seeing problems with pages not properly loading, +# disabling this option could work around the problem. +# +# Examples: +# +# tolerate-pipelining 1 +# +tolerate-pipelining 1 +# +# 6.6. default-server-timeout +# ============================ +# +# Specifies: +# +# Assumed server-side keep-alive timeout if not specified by the +# server. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections for which the server didn't specify the keep-alive +# timeout are not reused. +# +# Notes: +# +# Enabling this option significantly increases the number of +# connections that are reused, provided the keep-alive-timeout +# option is also enabled. +# +# While it also increases the number of connections problems +# when Privoxy tries to reuse a connection that already has been +# closed on the server side, or is closed while Privoxy is +# trying to reuse it, this should only be a problem if it +# happens for the first request sent by the client. If it +# happens for requests on reused client connections, Privoxy +# will simply close the connection and the client is supposed to +# retry the request without bothering the user. +# +# Enabling this option is therefore only recommended if the +# connection-sharing option is disabled. +# +# It is an error to specify a value larger than the +# keep-alive-timeout value. +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support. +# +# Examples: +# +# default-server-timeout 60 +# +#default-server-timeout 60 +# +# 6.7. connection-sharing +# ======================== +# +# Specifies: +# +# Whether or not outgoing connections that have been kept alive +# should be shared between different incoming connections. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# None +# +# Effect if unset: +# +# Connections are not shared. +# +# Notes: +# +# This option has no effect if Privoxy has been compiled without +# keep-alive support, or if it's disabled. +# +# Notes: +# +# Note that reusing connections doesn't necessary cause +# speedups. There are also a few privacy implications you should +# be aware of. +# +# If this option is effective, outgoing connections are shared +# between clients (if there are more than one) and closing the +# browser that initiated the outgoing connection does no longer +# affect the connection between Privoxy and the server unless +# the client's request hasn't been completed yet. +# +# If the outgoing connection is idle, it will not be closed +# until either Privoxy's or the server's timeout is reached. +# While it's open, the server knows that the system running +# Privoxy is still there. +# +# If there are more than one client (maybe even belonging to +# multiple users), they will be able to reuse each others +# connections. This is potentially dangerous in case of +# authentication schemes like NTLM where only the connection is +# authenticated, instead of requiring authentication for each +# request. +# +# If there is only a single client, and if said client can keep +# connections alive on its own, enabling this option has next to +# no effect. If the client doesn't support connection +# keep-alive, enabling this option may make sense as it allows +# Privoxy to keep outgoing connections alive even if the client +# itself doesn't support it. +# +# You should also be aware that enabling this option increases +# the likelihood of getting the "No server or forwarder data" +# error message, especially if you are using a slow connection +# to the Internet. +# +# This option should only be used by experienced users who +# understand the risks and can weight them against the benefits. +# +# Examples: +# +# connection-sharing 1 +# +#connection-sharing 1 +# +# 6.8. socket-timeout +# ==================== +# +# Specifies: +# +# Number of seconds after which a socket times out if no data is +# received. +# +# Type of value: +# +# Time in seconds. +# +# Default value: +# +# None +# +# Effect if unset: +# +# A default value of 300 seconds is used. +# +# Notes: +# +# The default is quite high and you probably want to reduce it. +# If you aren't using an occasionally slow proxy like Tor, +# reducing it to a few seconds should be fine. +# +# Examples: +# +# socket-timeout 300 +# +socket-timeout 300 +# +# 6.9. max-client-connections +# ============================ +# +# Specifies: +# +# Maximum number of client connections that will be served. +# +# Type of value: +# +# Positive number. +# +# Default value: +# +# 128 +# +# Effect if unset: +# +# Connections are served until a resource limit is reached. +# +# Notes: +# +# Privoxy creates one thread (or process) for every incoming +# client connection that isn't rejected based on the access +# control settings. +# +# If the system is powerful enough, Privoxy can theoretically +# deal with several hundred (or thousand) connections at the +# same time, but some operating systems enforce resource limits +# by shutting down offending processes and their default limits +# may be below the ones Privoxy would require under heavy load. +# +# Configuring Privoxy to enforce a connection limit below the +# thread or process limit used by the operating system makes +# sure this doesn't happen. Simply increasing the operating +# system's limit would work too, but if Privoxy isn't the only +# application running on the system, you may actually want to +# limit the resources used by Privoxy. +# +# If Privoxy is only used by a single trusted user, limiting the +# number of client connections is probably unnecessary. If there +# are multiple possibly untrusted users you probably still want +# to additionally use a packet filter to limit the maximal +# number of incoming connections per client. Otherwise a +# malicious user could intentionally create a high number of +# connections to prevent other users from using Privoxy. +# +# Obviously using this option only makes sense if you choose a +# limit below the one enforced by the operating system. +# +# One most POSIX-compliant systems Privoxy can't properly deal +# with more than FD_SETSIZE file descriptors at the same time +# and has to reject connections if the limit is reached. This +# will likely change in a future version, but currently this +# limit can't be increased without recompiling Privoxy with a +# different FD_SETSIZE limit. +# +# Examples: +# +# max-client-connections 256 +# +#max-client-connections 256 +# +# 6.10. handle-as-empty-doc-returns-ok +# ===================================== +# +# Specifies: +# +# The status code Privoxy returns for pages blocked with +# +handle-as-empty-document. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy returns a status 403(forbidden) for all blocked pages. +# +# Effect if set: +# +# Privoxy returns a status 200(OK) for pages blocked with +# +handle-as-empty-document and a status 403(Forbidden) for all +# other blocked pages. +# +# Notes: +# +# This directive was added as a work-around for Firefox bug +# 492459: "Websites are no longer rendered if SSL requests for +# JavaScripts are blocked by a proxy." +# (https://bugzilla.mozilla.org/show_bug.cgi?id=492459), the bug +# has been fixed for quite some time, but this directive is also +# useful to make it harder for websites to detect whether or not +# resources are being blocked. +# +#handle-as-empty-doc-returns-ok 1 +# +# 6.11. enable-compression +# ========================= +# +# Specifies: +# +# Whether or not buffered content is compressed before delivery. +# +# Type of value: +# +# 0 or 1 +# +# Default value: +# +# 0 +# +# Effect if unset: +# +# Privoxy does not compress buffered content. +# +# Effect if set: +# +# Privoxy compresses buffered content before delivering it to +# the client, provided the client supports it. +# +# Notes: +# +# This directive is only supported if Privoxy has been compiled +# with FEATURE_COMPRESSION, which should not to be confused with +# FEATURE_ZLIB. +# +# Compressing buffered content is mainly useful if Privoxy and +# the client are running on different systems. If they are +# running on the same system, enabling compression is likely to +# slow things down. If you didn't measure otherwise, you should +# assume that it does and keep this option disabled. +# +# Privoxy will not compress buffered content below a certain +# length. +# +#enable-compression 1 +# +# 6.12. compression-level +# ======================== +# +# Specifies: +# +# The compression level that is passed to the zlib library when +# compressing buffered content. +# +# Type of value: +# +# Positive number ranging from 0 to 9. +# +# Default value: +# +# 1 +# +# Notes: +# +# Compressing the data more takes usually longer than +# compressing it less or not compressing it at all. Which level +# is best depends on the connection between Privoxy and the +# client. If you can't be bothered to benchmark it for yourself, +# you should stick with the default and keep compression +# disabled. +# +# If compression is disabled, the compression level is +# irrelevant. +# +# Examples: +# +# # Best speed (compared to the other levels) +# compression-level 1 +# +# # Best compression +# compression-level 9 +# +# # No compression. Only useful for testing as the added header +# # slightly increases the amount of data that has to be sent. +# # If your benchmark shows that using this compression level +# # is superior to using no compression at all, the benchmark +# # is likely to be flawed. +# compression-level 0 +# +# +#compression-level 1 +# +# 6.13. client-header-order +# ========================== +# +# Specifies: +# +# The order in which client headers are sorted before forwarding +# them. +# +# Type of value: +# +# Client header names delimited by spaces or tabs +# +# Default value: +# +# None +# +# Notes: +# +# By default Privoxy leaves the client headers in the order they +# were sent by the client. Headers are modified in-place, new +# headers are added at the end of the already existing headers. +# +# The header order can be used to fingerprint client requests +# independently of other headers like the User-Agent. +# +# This directive allows to sort the headers differently to +# better mimic a different User-Agent. Client headers will be +# emitted in the order given, headers whose name isn't +# explicitly specified are added at the end. +# +# Note that sorting headers in an uncommon way will make +# fingerprinting actually easier. Encrypted headers are not +# affected by this directive. +# +#client-header-order Host \ +# Accept \ +# Accept-Language \ +# Accept-Encoding \ +# Proxy-Connection \ +# Referer \ +# Cookie \ +# DNT \ +# If-Modified-Since \ +# Cache-Control \ +# Content-Length \ +# Content-Type +# +# +# 7. WINDOWS GUI OPTIONS +# ======================= +# +# Privoxy has a number of options specific to the Windows GUI +# interface: +# +# +# +# If "activity-animation" is set to 1, the Privoxy icon will animate +# when "Privoxy" is active. To turn off, set to 0. +# +#activity-animation 1 +# +# +# +# If "log-messages" is set to 1, Privoxy copies log messages to the +# console window. The log detail depends on the debug directive. +# +#log-messages 1 +# +# +# +# If "log-buffer-size" is set to 1, the size of the log buffer, i.e. +# the amount of memory used for the log messages displayed in the +# console window, will be limited to "log-max-lines" (see below). +# +# Warning: Setting this to 0 will result in the buffer to grow +# infinitely and eat up all your memory! +# +#log-buffer-size 1 +# +# +# +# log-max-lines is the maximum number of lines held in the log +# buffer. See above. +# +#log-max-lines 200 +# +# +# +# If "log-highlight-messages" is set to 1, Privoxy will highlight +# portions of the log messages with a bold-faced font: +# +#log-highlight-messages 1 +# +# +# +# The font used in the console window: +# +#log-font-name Comic Sans MS +# +# +# +# Font size used in the console window: +# +#log-font-size 8 +# +# +# +# "show-on-task-bar" controls whether or not Privoxy will appear as +# a button on the Task bar when minimized: +# +#show-on-task-bar 0 +# +# +# +# If "close-button-minimizes" is set to 1, the Windows close button +# will minimize Privoxy instead of closing the program (close with +# the exit option on the File menu). +# +#close-button-minimizes 1 +# +# +# +# The "hide-console" option is specific to the MS-Win console +# version of Privoxy. If this option is used, Privoxy will +# disconnect from and hide the command console. +# +#hide-console +# +# +# diff --git a/roles/proxy/templates/usr.sbin.privoxy.j2 b/roles/proxy/templates/usr.sbin.privoxy.j2 new file mode 100644 index 00000000..5f8d9ddf --- /dev/null +++ b/roles/proxy/templates/usr.sbin.privoxy.j2 @@ -0,0 +1,15 @@ +#include + +/usr/sbin/privoxy { + #include + #include + + capability setgid, + capability setuid, + + /etc/privoxy/* r, + /etc/privoxy/templates/* r, + /run/privoxy.pid w, + /var/log/privoxy/logfile w, + +} From 263c3ac89df074a501db0019beadc335abc64ee5 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Wed, 17 Aug 2016 23:31:17 +0300 Subject: [PATCH 038/769] Fixes for #53 --- inventory | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory b/inventory index 75304a00..2f0f3adf 100644 --- a/inventory +++ b/inventory @@ -1,2 +1,2 @@ [localhost] -localhost ansible_connection=local +localhost ansible_connection=local ansible_python_interpreter=python From 08145447b311d2ebc129a072970b3293f5eeeed5 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Wed, 17 Aug 2016 23:38:34 +0300 Subject: [PATCH 039/769] default value for a public key #54 --- ec2.yml | 1 + gce.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/ec2.yml b/ec2.yml index 5339a2dd..d20626fd 100644 --- a/ec2.yml +++ b/ec2.yml @@ -54,6 +54,7 @@ - name: "ssh_public_key" prompt: "Enter the local path to your SSH public key (ex: ~/.ssh/id_rsa.pub):\n" + default: "~/.ssh/id_rsa.pub" private: no - name: "dns_enabled" diff --git a/gce.yml b/gce.yml index 093bfab2..6962f9fb 100644 --- a/gce.yml +++ b/gce.yml @@ -25,6 +25,7 @@ - name: "ssh_public_key" prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" + default: "~/.ssh/id_rsa.pub" private: no - name: "zone" From bb2691affe9b3451178e070d210af8d9b40baf12 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Wed, 17 Aug 2016 23:39:18 +0300 Subject: [PATCH 040/769] default value for a public key --- ec2.yml | 2 +- gce.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ec2.yml b/ec2.yml index d20626fd..c7018e54 100644 --- a/ec2.yml +++ b/ec2.yml @@ -53,7 +53,7 @@ private: no - name: "ssh_public_key" - prompt: "Enter the local path to your SSH public key (ex: ~/.ssh/id_rsa.pub):\n" + prompt: "Enter the local path to your SSH public key:\n" default: "~/.ssh/id_rsa.pub" private: no diff --git a/gce.yml b/gce.yml index 6962f9fb..f215e43d 100644 --- a/gce.yml +++ b/gce.yml @@ -24,7 +24,7 @@ private: no - name: "ssh_public_key" - prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" + prompt: "Enter the local path to your SSH public key:\n" default: "~/.ssh/id_rsa.pub" private: no From 429a21dd733acb277c22aff531f17f68f4524d24 Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan Reddy Naini Date: Wed, 17 Aug 2016 21:47:59 +0000 Subject: [PATCH 041/769] Digitalocean: Add new region blr1 Bangalore See https://www.digitalocean.com/company/blog/introducing-our-bangalore-region-blr1/ --- digitalocean.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/digitalocean.yml b/digitalocean.yml index 0954a7bd..8b54fb43 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -14,6 +14,7 @@ "9": "sfo2" "10": "sgp1" "11": "tor1" + "12": "blr1" vars_prompt: - name: "do_access_token" @@ -38,6 +39,7 @@ 9. San Francisco (Datacenter 2) 10. Singapore 11. Toronto + 12. Bangalore Enter the number of your desired region: default: "7" private: no From 7085a594fcfd4b66ea8c98ad1a2e70ec540bfefd Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 11:16:22 +0300 Subject: [PATCH 042/769] p12 moved into playbooks --- algo | 16 +++------------- digitalocean.yml | 5 +++++ ec2.yml | 5 +++++ gce.yml | 5 +++++ non-cloud.yml | 6 ++++++ roles/cloud-digitalocean/tasks/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 1 + roles/cloud-gce/tasks/main.yml | 1 + users.yml | 6 ++++++ 9 files changed, 33 insertions(+), 13 deletions(-) diff --git a/algo b/algo index 769c6b37..b6f096f7 100755 --- a/algo +++ b/algo @@ -1,12 +1,5 @@ #!/bin/sh -p12_export_password () { - echo -n " -Enter the password for p12 certificates (default: vpn): -: " - read -s P - P=${P:-vpn} -} algo_provisioning () { echo -n " @@ -29,14 +22,11 @@ Enter the number of your desired provider *) exit 1 ;; esac - p12_export_password - - ansible-playbook "${CLOUD}.yml" -e easyrsa_p12_export_password=${P} + ansible-playbook "${CLOUD}.yml" } - + user_management () { - p12_export_password - ansible-playbook users.yml -e easyrsa_p12_export_password=${P} + ansible-playbook users.yml } case "$1" in diff --git a/digitalocean.yml b/digitalocean.yml index 8b54fb43..f2f3d4ff 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -58,6 +58,11 @@ prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter the password for p12 certificates:\n" + default: "vpn" + private: yes roles: - cloud-digitalocean diff --git a/ec2.yml b/ec2.yml index c7018e54..f880d0ff 100644 --- a/ec2.yml +++ b/ec2.yml @@ -66,6 +66,11 @@ prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter the password for p12 certificates:\n" + default: "vpn" + private: yes roles: - cloud-ec2 diff --git a/gce.yml b/gce.yml index f215e43d..3c8e9f73 100644 --- a/gce.yml +++ b/gce.yml @@ -62,6 +62,11 @@ prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter the password for p12 certificates:\n" + default: "vpn" + private: yes roles: - cloud-gce diff --git a/non-cloud.yml b/non-cloud.yml index b53ece7d..573f7fea 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -22,6 +22,11 @@ prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter the password for p12 certificates:\n" + default: "vpn" + private: yes tasks: - name: Add the server to the vpn-host group @@ -32,6 +37,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 0fa41204..576fd615 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -33,6 +33,7 @@ do_droplet_id: "{{ do.droplet.id }}" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 75bd4f2e..3c067cc1 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -71,6 +71,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" with_items: "{{ ec2.instances }}" - name: Wait for SSH to become available diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 62f55402..72b1abf5 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -22,6 +22,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - name: Firewall configured local_action: diff --git a/users.yml b/users.yml index a7489c0e..80f0998a 100644 --- a/users.yml +++ b/users.yml @@ -14,6 +14,11 @@ prompt: "What user should we use to login on the server?:\n" default: "root" private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter the password for p12 certificates:\n" + default: "vpn" + private: yes tasks: - name: Add the server to the vpn-host group @@ -22,6 +27,7 @@ groupname: vpn-host ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" From a1bf2ad5efddabd272d262a89c085692e20802df Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 11:22:06 +0300 Subject: [PATCH 043/769] flush handlers after loopback configured --- roles/common/tasks/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index afdc91fb..addb7c71 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -77,4 +77,6 @@ - name: Loopback included into the network config lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present notify: - - restart loopback + - restart loopback + +- meta: flush_handlers From 66c60444960ec63e410685d3b31d9bd039a857c6 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 11:24:00 +0300 Subject: [PATCH 044/769] bash compatibility --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index b6f096f7..4b575d02 100755 --- a/algo +++ b/algo @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash algo_provisioning () { From d914b566afa6299d1caa2093d167aab03f415772 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 11:36:36 +0300 Subject: [PATCH 045/769] Reame for local installation #52 --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index def5464a..ec537e5a 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,18 @@ Start the deploy and follow the instructions: ./algo ``` -When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. +When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. + +### Deploy Algo locally + +In order to install algo on Ubuntu locally, you need to install ansible first. +Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. +It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. +Therefore, to use apt you must use the ansible PPA and using a PPA requires installing `software-properties-common` +``` +sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible +sudo apt-get update && sudo apt-get install ansible +``` ### User Management From e618cdf0ce3bc7109e1dcd9810917837ca965c4b Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 11:48:30 +0300 Subject: [PATCH 046/769] Change some prompts #52 --- algo | 2 +- non-cloud.yml | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/algo b/algo index 4b575d02..2676faca 100755 --- a/algo +++ b/algo @@ -7,7 +7,7 @@ algo_provisioning () { 1. DigitalOcean 2. Amazon EC2 3. Google Compute Engine - 4. Remote installation (install to existing Ubuntu server) + 4. Install to existing Ubuntu server Enter the number of your desired provider : " diff --git a/non-cloud.yml b/non-cloud.yml index 573f7fea..40842b2e 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -5,11 +5,11 @@ vars_prompt: - name: "server_ip" - prompt: "Enter IP address of your server:\n" + prompt: "Enter IP address of your server: (use localhost for local installation)\n" private: no - name: "server_user" - prompt: "What user should we use to login on the server?:\n" + prompt: "What user should we use to login on the server? (Ignore if you're deploying to localhost):\n" default: "root" private: no @@ -39,10 +39,6 @@ auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - - name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ server_ip }} timeout=320" - become: false - - name: Post-provisioning tasks hosts: vpn-host gather_facts: false From a9b10baf1deb070d6212947b2d2443daf3a4a4f4 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 12:17:46 +0300 Subject: [PATCH 047/769] Some fixes --- config.cfg | 1 + non-cloud.yml | 14 ++++++++++---- roles/vpn/tasks/main.yml | 6 +++--- roles/vpn/templates/easy-rsa.vars.j2 | 4 ++-- roles/vpn/templates/ipsec.conf.j2 | 4 ++-- roles/vpn/templates/ipsec.secrets.j2 | 2 +- roles/vpn/templates/mobileconfig.j2 | 12 ++++++------ 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/config.cfg b/config.cfg index 4daa91a0..cb161eec 100644 --- a/config.cfg +++ b/config.cfg @@ -10,6 +10,7 @@ easyrsa_reinit_existent: False vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4021:69ce::/64' server_name: "{{ ansible_ssh_host }}" +IP_subject_alt_name: "{{ ansible_ssh_host }}" # Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) service_dns: True diff --git a/non-cloud.yml b/non-cloud.yml index 40842b2e..a68b0140 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -6,6 +6,7 @@ - name: "server_ip" prompt: "Enter IP address of your server: (use localhost for local installation)\n" + default: localhost private: no - name: "server_user" @@ -26,7 +27,11 @@ - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" - private: yes + private: yes + + - name: "IP_subject_alt_name" + prompt: "Enter public IP address of your server: (IMPORTANT! This IP is using to verify the certificate)\n" + private: no tasks: - name: Add the server to the vpn-host group @@ -38,6 +43,7 @@ dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" - name: Post-provisioning tasks hosts: vpn-host @@ -53,9 +59,9 @@ raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 roles: - - common - - security - - proxy + #- common + #- security + #- proxy - vpn - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 8bbbcb50..3751ea22 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -67,7 +67,7 @@ - name: Build the server pair shell: > - ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ ansible_ssh_host }}' build-server-full {{ ansible_ssh_host }} nopass&& + ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass&& touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' @@ -99,12 +99,12 @@ - restart strongswan - name: Copy the server cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ ansible_ssh_host }}.crt' dest=/etc/ipsec.d/certs/{{ ansible_ssh_host }}.crt owner=root group=root mode=0600 + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=root group=root mode=0600 notify: - restart strongswan - name: Copy the server key to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ ansible_ssh_host }}.key' dest=/etc/ipsec.d/private/{{ ansible_ssh_host }}.key owner=root group=root mode=0600 + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=root group=root mode=0600 notify: - restart strongswan diff --git a/roles/vpn/templates/easy-rsa.vars.j2 b/roles/vpn/templates/easy-rsa.vars.j2 index f46993fb..50159aa6 100644 --- a/roles/vpn/templates/easy-rsa.vars.j2 +++ b/roles/vpn/templates/easy-rsa.vars.j2 @@ -183,7 +183,7 @@ set_var EASYRSA_SSL_CONF "$EASYRSA/openssl-1.0.cnf" # This is best left alone. Interactively you will set this manually, and BATCH # callers are expected to set this themselves. -set_var EASYRSA_REQ_CN "{{ ansible_ssh_host }}" +set_var EASYRSA_REQ_CN "{{ IP_subject_alt_name }}" # Cryptographic digest to use. # Do not change this default unless you understand the security implications. @@ -195,4 +195,4 @@ set_var EASYRSA_REQ_CN "{{ ansible_ssh_host }}" # in batch mode without any user input, confirmation on dangerous operations, # or most output. Setting this to any non-blank string enables batch mode. -set_var EASYRSA_BATCH "{{ ansible_ssh_host }}" +set_var EASYRSA_BATCH "{{ IP_subject_alt_name }}" diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 8bb61817..cd00596c 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -15,8 +15,8 @@ conn %default left=%any leftauth=pubkey - leftid={{ ansible_ssh_host }} - leftcert={{ ansible_ssh_host }}.crt + leftid={{ IP_subject_alt_name }} + leftcert={{ IP_subject_alt_name }}.crt leftsendcert=always leftsubnet=0.0.0.0/0,::/0 diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/vpn/templates/ipsec.secrets.j2 index cc208a59..d5793aea 100644 --- a/roles/vpn/templates/ipsec.secrets.j2 +++ b/roles/vpn/templates/ipsec.secrets.j2 @@ -1,2 +1,2 @@ -: ECDSA {{ ansible_ssh_host }}.key +: ECDSA {{ IP_subject_alt_name }}.key diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index d1a235c6..3fc3668b 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -48,11 +48,11 @@ CertificateType ECDSA256 ServerCertificateIssuerCommonName - {{ ansible_ssh_host }} + {{ IP_subject_alt_name }} RemoteAddress - {{ ansible_ssh_host }} + {{ IP_subject_alt_name }} RemoteIdentifier - {{ ansible_ssh_host }} + {{ IP_subject_alt_name }} UseConfigurationAttributeInternalIPSubnet 0 @@ -81,7 +81,7 @@ 0 UserDefinedName - {{ ansible_ssh_host }} IKEv2 + {{ IP_subject_alt_name }} IKEv2 VPNType IKEv2 @@ -117,7 +117,7 @@ PayloadDescription Adds a CA root certificate PayloadDisplayName - {{ ansible_ssh_host }} + {{ IP_subject_alt_name }} PayloadIdentifier com.apple.security.root.32EA3AAA-D19E-43EF-B357-608218745A38 PayloadType @@ -129,7 +129,7 @@ PayloadDisplayName - {{ ansible_ssh_host }} IKEv2 + {{ IP_subject_alt_name }} IKEv2 PayloadIdentifier donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BE PayloadRemovalDisallowed From f20d375dc91650ab00a7cb143e696e61d2a7483a Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 12:32:28 +0300 Subject: [PATCH 048/769] IP_subject fixes --- non-cloud.yml | 6 ++++-- roles/vpn/tasks/main.yml | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/non-cloud.yml b/non-cloud.yml index a68b0140..b89c2827 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -29,7 +29,7 @@ default: "vpn" private: yes - - name: "IP_subject_alt_name" + - name: "IP_subject" prompt: "Enter public IP address of your server: (IMPORTANT! This IP is using to verify the certificate)\n" private: no @@ -43,7 +43,7 @@ dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - IP_subject_alt_name: "{{ IP_subject_alt_name }}" + IP_subject: "{{ IP_subject }}" - name: Post-provisioning tasks hosts: vpn-host @@ -57,6 +57,8 @@ raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + - set_fact: + IP_subject_alt_name: "{{ IP_subject }}" roles: #- common diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 3751ea22..612f4c57 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,3 +1,6 @@ +- name: Gather Facts + setup: + - name: Install StrongSwan apt: name=strongswan state=latest update_cache=yes From 9eaaf63fa0cc9c778d42ee6807afaeee0c9b6c22 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 12:36:54 +0300 Subject: [PATCH 049/769] server_name fixes --- roles/vpn/tasks/main.yml | 6 +++--- users.yml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 612f4c57..cf045a46 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -130,14 +130,14 @@ no_log: True - name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" - name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ server_name }}_{{ item }}.mobileconfig flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" - name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes notify: - congrats diff --git a/users.yml b/users.yml index 80f0998a..53b4b354 100644 --- a/users.yml +++ b/users.yml @@ -61,7 +61,7 @@ - name: Get active users shell: > - grep ^V pki/index.txt | grep -v "{{ server_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + grep ^V pki/index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' register: valid_certs @@ -95,12 +95,12 @@ no_log: True - name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ server_name }}_{{ item }}.p12 flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" - name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ server_name }}_{{ item }}.mobileconfig flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" - name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ server_name }}_ca.crt flat=yes + fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes From f3eb06cfe06bc458bf511cf03062594db5518cf7 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 12:44:34 +0300 Subject: [PATCH 050/769] server_name fixes --- roles/vpn/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index cf045a46..d9f64bf5 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -70,7 +70,7 @@ - name: Build the server pair shell: > - ./easyrsa --subject-alt-name='DNS:{{ server_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass&& + ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass&& touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' From b4f55ceee11cae6002579f7c1a966010096ad9bf Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 12:52:28 +0300 Subject: [PATCH 051/769] Local installation #52 --- non-cloud.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/non-cloud.yml b/non-cloud.yml index b89c2827..d675ebd3 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -61,9 +61,9 @@ IP_subject_alt_name: "{{ IP_subject }}" roles: - #- common - #- security - #- proxy + - common + - security + - proxy - vpn - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } From 64d735a383861f8a2594e5b529147631c2da8f81 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 18 Aug 2016 14:08:06 -0400 Subject: [PATCH 052/769] Update non-cloud.yml --- non-cloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/non-cloud.yml b/non-cloud.yml index d675ebd3..1623a551 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -10,7 +10,7 @@ private: no - name: "server_user" - prompt: "What user should we use to login on the server? (Ignore if you're deploying to localhost):\n" + prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" default: "root" private: no @@ -30,7 +30,7 @@ private: yes - name: "IP_subject" - prompt: "Enter public IP address of your server: (IMPORTANT! This IP is using to verify the certificate)\n" + prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" private: no tasks: From 3278a9e65c5a692317ef74b91a48bd1d90ae7fe7 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 18 Aug 2016 14:15:15 -0400 Subject: [PATCH 053/769] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ec537e5a..f66080e9 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * SHell or BASH * libselinux-python (for RedHat based distros) -### Initial Deployment +### Cloud Deployment To install the dependencies on OS X or Linux: @@ -57,15 +57,14 @@ Start the deploy and follow the instructions: When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. -### Deploy Algo locally +### Local Deployment -In order to install algo on Ubuntu locally, you need to install ansible first. -Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. -It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. -Therefore, to use apt you must use the ansible PPA and using a PPA requires installing `software-properties-common` +It is possible to download Algo to your own Ubuntu server and run the scripts locally. You need to install ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA and using a PPA requires installing `software-properties-common`. tl;dr: ``` sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible sudo apt-get update && sudo apt-get install ansible +git clone git@github.com:trailofbits/algo.git +cd algo && ./algo ``` ### User Management From 16627783f55150a497b3c93738923734d9c98c7d Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 21:35:47 +0300 Subject: [PATCH 054/769] Minor updates to the sshd_config #51 --- roles/common/tasks/main.yml | 13 +--- roles/common/templates/sshd_config.j2 | 106 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 roles/common/templates/sshd_config.j2 diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index addb7c71..b1e397a2 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -30,19 +30,10 @@ when: reboot_required is defined and reboot_required.stdout == 'required' become: false -# SSH fixes - - name: SSH config - lineinfile: dest="{{ item.file }}" regexp="{{ item.regexp }}" line="{{ item.line }}" state=present - with_items: - - { regexp: '^PasswordAuthentication.*', line: 'PasswordAuthentication no', file: '/etc/ssh/sshd_config' } - - { regexp: '^PermitRootLogin.*', line: 'PermitRootLogin without-password', file: '/etc/ssh/sshd_config' } - - { regexp: '^UseDNS.*', line: 'UseDNS no', file: '/etc/ssh/sshd_config' } - - { regexp: '^Ciphers', line: 'Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com', file: '/etc/ssh/sshd_config' } - - { regexp: '^MACs', line: 'MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com', file: '/etc/ssh/sshd_config' } - - { regexp: '^KexAlgorithms', line: 'KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256', file: '/etc/ssh/sshd_config' } + template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 notify: - - restart ssh + - restart ssh - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 new file mode 100644 index 00000000..65b43560 --- /dev/null +++ b/roles/common/templates/sshd_config.j2 @@ -0,0 +1,106 @@ +# Package generated configuration file +# See the sshd_config(5) manpage for details + +# What ports, IPs and protocols we listen for +Port 22 + +# Use these options to restrict which interfaces/protocols sshd will bind to +#ListenAddress :: +#ListenAddress 0.0.0.0 +Protocol 2 + +# HostKeys for protocol version 2 +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key + +# Use kernel sandbox mechanisms where possible in unprivilegied processes +# Systrace on OpenBSD, Seccomp on Linux, seatbelt on MacOSX/Darwin, rlimit elsewhere. +UsePrivilegeSeparation sandbox + +# Lifetime and size of ephemeral version 1 server key +KeyRegenerationInterval 3600 +ServerKeyBits 1024 + +# Logging +SyslogFacility AUTH +LogLevel INFO + +# Authentication: +LoginGraceTime 120 +PermitRootLogin without-password +StrictModes yes + +RSAAuthentication yes +PubkeyAuthentication yes +#AuthorizedKeysFile %h/.ssh/authorized_keys + +# Don't read the user's ~/.rhosts and ~/.shosts files +IgnoreRhosts yes + +# For this to work you will also need host keys in /etc/ssh_known_hosts +RhostsRSAAuthentication no + +# similar for protocol version 2 +HostbasedAuthentication no + +# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication +#IgnoreUserKnownHosts yes + +# To enable empty passwords, change to yes (NOT RECOMMENDED) +PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +ChallengeResponseAuthentication no + +# Change to no to disable tunnelled clear text passwords +PasswordAuthentication no + +# Kerberos options +#KerberosAuthentication no +#KerberosGetAFSToken no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes + +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +#UseLogin no + +#MaxStartups 10:30:60 +#Banner /etc/issue.net + +# Allow client to pass locale environment variables +AcceptEnv LANG LC_* + +Subsystem sftp /usr/lib/openssh/sftp-server + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin yes +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +UsePAM yes + +# Added by DigitalOcean build process +ClientAliveInterval 120 +ClientAliveCountMax 2 +UseDNS no +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com +MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com +KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 + +# Password based logins are disabled - only public key based logins are allowed. +AuthenticationMethods publickey From cd706dbf8290014b4b404255a61f6e4ed886b0a3 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 21:49:20 +0300 Subject: [PATCH 055/769] Update-users fixed #52 --- users.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/users.yml b/users.yml index 53b4b354..2e9e37e2 100644 --- a/users.yml +++ b/users.yml @@ -7,18 +7,23 @@ vars_prompt: - name: "server_ip" - prompt: "\nEnter IP address of your server:\n" + prompt: "Enter IP address of your server: (use localhost for local installation)\n" + default: localhost private: no - name: "server_user" - prompt: "What user should we use to login on the server?:\n" + prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" default: "root" private: no - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" - private: yes + private: yes + + - name: "IP_subject" + prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" + private: no tasks: - name: Add the server to the vpn-host group @@ -28,6 +33,7 @@ ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + IP_subject: "{{ IP_subject }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" @@ -39,6 +45,10 @@ become: true vars_files: - config.cfg + + pre_tasks: + - set_fact: + IP_subject_alt_name: "{{ IP_subject }}" tasks: - name: Build the client's pair From 68a5ae8453ddc7bf7c37f208c77ee83faefd6926 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Thu, 18 Aug 2016 22:13:18 +0300 Subject: [PATCH 056/769] README fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f66080e9..b4a8ccee 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ It is possible to download Algo to your own Ubuntu server and run the scripts lo ``` sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible sudo apt-get update && sudo apt-get install ansible -git clone git@github.com:trailofbits/algo.git +git clone https://github.com/trailofbits/algo cd algo && ./algo ``` From 4a6602e877d61692b69610c9a271edb66fdc741f Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 14:14:09 +0300 Subject: [PATCH 057/769] RSAAuthentication no; Turn off SFTP; Turn off X11 forwarding; #51 --- roles/common/templates/sshd_config.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index 65b43560..d17d9f67 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -32,7 +32,7 @@ LoginGraceTime 120 PermitRootLogin without-password StrictModes yes -RSAAuthentication yes +RSAAuthentication no PubkeyAuthentication yes #AuthorizedKeysFile %h/.ssh/authorized_keys @@ -68,7 +68,7 @@ PasswordAuthentication no #GSSAPIAuthentication no #GSSAPICleanupCredentials yes -X11Forwarding yes +X11Forwarding no X11DisplayOffset 10 PrintMotd no PrintLastLog yes @@ -81,7 +81,7 @@ TCPKeepAlive yes # Allow client to pass locale environment variables AcceptEnv LANG LC_* -Subsystem sftp /usr/lib/openssh/sftp-server +# Subsystem sftp /usr/lib/openssh/sftp-server # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will From ae33103ca8d7d33e176cfd01dc9ca1ff111b2125 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 14:23:06 +0300 Subject: [PATCH 058/769] Make a requirements.txt for pip #58 --- README.md | 2 +- requirements.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index b4a8ccee..1f8612bb 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ To install the dependencies on OS X or Linux: ``` sudo easy_install pip -sudo pip install ansible dopy==0.3.5 boto apache-libcloud six +sudo pip install -r requirements.txt ``` Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a666d82a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +dopy==0.3.5 +boto +azure>=0.7.1 +apache-libcloud +six From cfc38e3df1c1cb4fc405438a911a656a8395527e Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 15:19:46 +0300 Subject: [PATCH 059/769] Drop SMB traffic ##61 --- roles/cloud-digitalocean/tasks/main.yml | 2 ++ roles/cloud-ec2/tasks/main.yml | 2 ++ roles/cloud-gce/tasks/main.yml | 2 ++ roles/dns_adblocking/tasks/main.yml | 2 +- roles/security/handlers/main.yml | 5 ++++- roles/security/tasks/main.yml | 17 +++++++++++++++++ 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 576fd615..7bdee8be 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -34,6 +34,8 @@ dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: digitalocean + ipv6_support: yes - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 3c067cc1..dd657534 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -72,6 +72,8 @@ dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: ec2 + ipv6_support: no with_items: "{{ ec2.instances }}" - name: Wait for SSH to become available diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 72b1abf5..4ab0ee23 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -23,6 +23,8 @@ dns_enabled: "{{ dns_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: gce + ipv6_support: no - name: Firewall configured local_action: diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index fcc5589d..8ff6ed90 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -50,7 +50,7 @@ to_destination: fcaa::1:53 ip_version: ipv6 notify: - - save iptables + - save iptables - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index da5c0922..2b8e5ad6 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,5 +1,8 @@ - name: restart rsyslog service: name=rsyslog state=restarted - + +- name: save iptables + command: service netfilter-persistent save + - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 071f6ff1..c9ce0558 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -98,3 +98,20 @@ - name: Do not send ICMP redirects (we are not a router) sysctl: name=net.ipv4.conf.all.send_redirects value=0 + +- name: Drop SMB traffic + iptables: + table: filter + chain: FORWARD + protocol: tcp + source: 0.0.0.0/0 + destination: 0.0.0.0/0 + destination_port: "{{ item }}" + jump: DROP + action: insert + with_items: + - 137 + - 139 + - 445 + notify: + - save iptables From 3fa75a081d5ae82711d4c1f3b5545b5942e9cecf Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 16:22:14 +0300 Subject: [PATCH 060/769] new iptabes deployment #61 --- ansible.cfg | 1 + roles/common/templates/sshd_config.j2 | 2 +- roles/dns_adblocking/handlers/main.yml | 3 - roles/dns_adblocking/tasks/main.yml | 29 +--- roles/security/handlers/main.yml | 4 +- roles/security/tasks/main.yml | 221 ++++++++++++------------- roles/security/templates/rules.v4.j2 | 32 ++++ roles/security/templates/rules.v6.j2 | 43 +++++ roles/vpn/handlers/main.yml | 3 - roles/vpn/tasks/main.yml | 10 -- 10 files changed, 186 insertions(+), 162 deletions(-) create mode 100644 roles/security/templates/rules.v4.j2 create mode 100644 roles/security/templates/rules.v6.j2 diff --git a/ansible.cfg b/ansible.cfg index 4abecdfe..dc8f8cd4 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -9,3 +9,4 @@ record_host_keys = False [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null +scp_if_ssh = True diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index d17d9f67..8a972b5a 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -81,7 +81,7 @@ TCPKeepAlive yes # Allow client to pass locale environment variables AcceptEnv LANG LC_* -# Subsystem sftp /usr/lib/openssh/sftp-server +Subsystem sftp /usr/lib/openssh/sftp-server # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will diff --git a/roles/dns_adblocking/handlers/main.yml b/roles/dns_adblocking/handlers/main.yml index 562427dc..98278cef 100644 --- a/roles/dns_adblocking/handlers/main.yml +++ b/roles/dns_adblocking/handlers/main.yml @@ -3,6 +3,3 @@ - name: restart apparmor service: name=apparmor state=restarted - -- name: save iptables - command: service netfilter-persistent save diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 8ff6ed90..aa72a4ce 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -27,33 +27,6 @@ shell: > /opt/adblock.sh -- name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network }}" - jump: DNAT - to_destination: 172.16.0.1:53 - notify: - - save iptables - -- name: Forward all DNS requests to the local resolver - iptables: - table: nat - chain: PREROUTING - protocol: udp - destination_port: 53 - source: "{{ vpn_network_ipv6 }}" - jump: DNAT - to_destination: fcaa::1:53 - ip_version: ipv6 - notify: - - save iptables - - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes - -- name: Dnsmasq disabled and stopped - service: name=dnsmasq state=stopped enabled=no + diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index 2b8e5ad6..f5fb1c9a 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,8 +1,8 @@ - name: restart rsyslog service: name=rsyslog state=restarted -- name: save iptables - command: service netfilter-persistent save +- name: restart iptables + service: name=netfilter-persistent state=restarted - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index c9ce0558..9f5a665a 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,117 +1,108 @@ -# Using a two-pass approach for checking directories in order to support symlinks. -- name: Find directories for minimizing access - stat: - path: "{{ item }}" - register: minimize_access_directories +## Using a two-pass approach for checking directories in order to support symlinks. +#- name: Find directories for minimizing access + #stat: + #path: "{{ item }}" + #register: minimize_access_directories + #with_items: + #- '/usr/local/sbin' + #- '/usr/local/bin' + #- '/usr/sbin' + #- '/usr/bin' + #- '/sbin' + #- '/bin' + +#- name: Minimize access + #file: path='{{ item.stat.path }}' mode='go-w' recurse=yes + #when: item.stat.isdir + #with_items: "{{ minimize_access_directories.results }}" + #no_log: True + +#- name: Change shadow ownership to root and mode to 0600 + #file: dest='/etc/shadow' owner=root group=root mode=0600 + +#- name: change su-binary to only be accessible to user and group root + #file: dest='/bin/su' owner=root group=root mode=0750 + +#- name: Collect Use of privileged commands + #shell: > + #/usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' + #args: + #executable: /bin/bash + #register: privileged_programs + +## Rsyslog + +#- name: Rsyslog configured + #template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf + #notify: + #- restart rsyslog + +#- name: Rsyslog CIS configured + #template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 + #notify: + #- restart rsyslog + +#- name: Enable services + #service: name=rsyslog enabled=yes + +## Core dumps + +#- name: Restrict core dumps (with PAM) + #lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present + +#- name: Restrict core dumps (with sysctl) + #sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + +## Kernel fixes + +#- name: Disable Source Routed Packet Acceptance + #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + #with_items: + #- net.ipv4.conf.all.accept_source_route + #- net.ipv4.conf.default.accept_source_route + #notify: + #- flush routing cache + +#- name: Disable ICMP Redirect Acceptance + #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + #with_items: + #- net.ipv4.conf.all.accept_redirects + #- net.ipv4.conf.default.accept_redirects + +#- name: Disable Secure ICMP Redirect Acceptance + #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + #with_items: + #- net.ipv4.conf.all.secure_redirects + #- net.ipv4.conf.default.secure_redirects + #notify: + #- flush routing cache + +#- name: Enable Bad Error Message Protection + #sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + #notify: + #- flush routing cache + +#- name: Enable RFC-recommended Source Route Validation + #sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + #with_items: + #- net.ipv4.conf.all.rp_filter + #- net.ipv4.conf.default.rp_filter + #notify: + #- flush routing cache + +#- name: Enable packet forwarding for IPv4 + #sysctl: name=net.ipv4.ip_forward value=1 + +#- name: Enable packet forwarding for IPv6 + #sysctl: name=net.ipv6.conf.all.forwarding value=1 + +#- name: Do not send ICMP redirects (we are not a router) + #sysctl: name=net.ipv4.conf.all.send_redirects value=0 + +- name: Iptables configured + template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 with_items: - - '/usr/local/sbin' - - '/usr/local/bin' - - '/usr/sbin' - - '/usr/bin' - - '/sbin' - - '/bin' - -- name: Minimize access - file: path='{{ item.stat.path }}' mode='go-w' recurse=yes - when: item.stat.isdir - with_items: "{{ minimize_access_directories.results }}" - no_log: True - -- name: Change shadow ownership to root and mode to 0600 - file: dest='/etc/shadow' owner=root group=root mode=0600 - -- name: change su-binary to only be accessible to user and group root - file: dest='/bin/su' owner=root group=root mode=0750 - -- name: Collect Use of privileged commands - shell: > - /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' - args: - executable: /bin/bash - register: privileged_programs - -# Rsyslog - -- name: Rsyslog configured - template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf + - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } + - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: - - restart rsyslog - -- name: Rsyslog CIS configured - template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 - notify: - - restart rsyslog - -- name: Enable services - service: name=rsyslog enabled=yes - -# Core dumps - -- name: Restrict core dumps (with PAM) - lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present - -- name: Restrict core dumps (with sysctl) - sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - -# Kernel fixes - -- name: Disable Source Routed Packet Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_source_route - - net.ipv4.conf.default.accept_source_route - notify: - - flush routing cache - -- name: Disable ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects - -- name: Disable Secure ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.secure_redirects - - net.ipv4.conf.default.secure_redirects - notify: - - flush routing cache - -- name: Enable Bad Error Message Protection - sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - notify: - - flush routing cache - -- name: Enable RFC-recommended Source Route Validation - sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.rp_filter - - net.ipv4.conf.default.rp_filter - notify: - - flush routing cache - -- name: Enable packet forwarding for IPv4 - sysctl: name=net.ipv4.ip_forward value=1 - -- name: Enable packet forwarding for IPv6 - sysctl: name=net.ipv6.conf.all.forwarding value=1 - -- name: Do not send ICMP redirects (we are not a router) - sysctl: name=net.ipv4.conf.all.send_redirects value=0 - -- name: Drop SMB traffic - iptables: - table: filter - chain: FORWARD - protocol: tcp - source: 0.0.0.0/0 - destination: 0.0.0.0/0 - destination_port: "{{ item }}" - jump: DROP - action: insert - with_items: - - 137 - - 139 - - 445 - notify: - - save iptables + - restart iptables diff --git a/roles/security/templates/rules.v4.j2 b/roles/security/templates/rules.v4.j2 new file mode 100644 index 00000000..46fbe85c --- /dev/null +++ b/roles/security/templates/rules.v4.j2 @@ -0,0 +1,32 @@ +*nat +:PREROUTING ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +{% if dns_enabled is defined and dns_enabled == "Y" %} +-A PREROUTING -s {{ vpn_network }} -p udp -m udp --dport 53 -j DNAT --to-destination 172.16.0.1:53 +{% endif %} +-A POSTROUTING -s {{ vpn_network }} -m policy --pol none --dir out -j MASQUERADE +COMMIT +*filter +:INPUT DROP [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [0:0] +-A INPUT -i lo -j ACCEPT +-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A INPUT -p esp -j ACCEPT +-A INPUT -p ah -j ACCEPT +# rate limit ICMP traffic per source +-A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP +-A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +# TODO: +# The IP of the resolver should be bound to a DUMMY interface. +# DUMMY interfaces are the proper way to install IPs without assigning them any +# particular virtual (tun,tap,...) or physical (ethernet) interface. +-A INPUT -d 172.16.0.1 -p udp --dport 53 -j ACCEPT +-A INPUT -d 172.16.0.1 -p tcp -m multiport --dport 8080,8118 -j ACCEPT +-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A FORWARD -p tcp --dport 445 -j DROP +-A FORWARD -p udp -m multiport --ports 137,138 -j DROP +-A FORWARD -p tcp -m multiport --ports 137,139 -j DROP +-A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT +COMMIT diff --git a/roles/security/templates/rules.v6.j2 b/roles/security/templates/rules.v6.j2 new file mode 100644 index 00000000..14449cf7 --- /dev/null +++ b/roles/security/templates/rules.v6.j2 @@ -0,0 +1,43 @@ +*nat +:PREROUTING ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +{% if dns_enabled is defined and dns_enabled == "Y" %} +-A PREROUTING -s {{ vpn_network_ipv6 }} -p udp -m udp --dport 53 -j DNAT --to-destination fcaa::1:53 +{% endif %} +-A POSTROUTING -s {{ vpn_network_ipv6 }} -m policy --pol none --dir out -j MASQUERADE +COMMIT +*filter +:INPUT DROP [0:0] +:FORWARD DROP [0:0] +:OUTPUT DROP [0:0] +:ICMPV6-CHECK - [0:0] +:ICMPV6-CHECK-LOG - [0:0] +-A INPUT -i lo -j ACCEPT +-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A INPUT -p esp -j ACCEPT +-A INPUT -m ah -j ACCEPT +# rate limit ICMP traffic per source +-A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP +-A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +-A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +# TODO: +# The IP of the resolver should be bound to a DUMMY interface. +# DUMMY interfaces are the proper way to install IPs without assigning them any +# particular virtual (tun,tap,...) or physical (ethernet) interface. +-A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT +-A FORWARD -j ICMPV6-CHECK +-A FORWARD -p tcp --dport 445 -j DROP +-A FORWARD -p udp -m multiport --ports 137,138 -j DROP +-A FORWARD -p tcp -m multiport --ports 137,139 -j DROP +-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +-A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +# this is so potential malicious traffic can not reach anywhere over the server +# https://www.insinuator.net/2016/05/cve-2016-1409-ipv6-ndp-dos-vulnerability-in-cisco-software/ +# other software implementations might be at least as broken as the one in CISCO gear. +-A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG +-A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG +-A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG +-A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG +-A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " +-A ICMPV6-CHECK-LOG -j DROP +COMMIT diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 0885344e..94ccdfca 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -4,9 +4,6 @@ - name: restart apparmor service: name=apparmor state=restarted -- name: save iptables - command: service netfilter-persistent save - - name: congrats debug: msg: diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index d9f64bf5..b21f52ba 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -20,16 +20,6 @@ - strongswan - netfilter-persistent -- name: Configure iptables so IPSec traffic can traverse the tunnel - iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE - notify: - - save iptables - -- name: Configure ip6tables so IPSec traffic can traverse the tunnel - iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - notify: - - save iptables - - name: Setup the ipsec.conf file from our template template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 notify: From b593986b0cfcbadc8d5b752da92700cbe36a21c6 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 16:22:54 +0300 Subject: [PATCH 061/769] SFTP fixed --- roles/common/templates/sshd_config.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index 8a972b5a..d17d9f67 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -81,7 +81,7 @@ TCPKeepAlive yes # Allow client to pass locale environment variables AcceptEnv LANG LC_* -Subsystem sftp /usr/lib/openssh/sftp-server +# Subsystem sftp /usr/lib/openssh/sftp-server # Set this to 'yes' to enable PAM authentication, account processing, # and session processing. If this is enabled, PAM authentication will From de06b4fd9e10050544f2c723318f8351136b2490 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 16:24:00 +0300 Subject: [PATCH 062/769] security remarks --- roles/security/tasks/main.yml | 160 +++++++++++++++++----------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 9f5a665a..1ef078ab 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,103 +1,103 @@ -## Using a two-pass approach for checking directories in order to support symlinks. -#- name: Find directories for minimizing access - #stat: - #path: "{{ item }}" - #register: minimize_access_directories - #with_items: - #- '/usr/local/sbin' - #- '/usr/local/bin' - #- '/usr/sbin' - #- '/usr/bin' - #- '/sbin' - #- '/bin' +# Using a two-pass approach for checking directories in order to support symlinks. +- name: Find directories for minimizing access + stat: + path: "{{ item }}" + register: minimize_access_directories + with_items: + - '/usr/local/sbin' + - '/usr/local/bin' + - '/usr/sbin' + - '/usr/bin' + - '/sbin' + - '/bin' -#- name: Minimize access - #file: path='{{ item.stat.path }}' mode='go-w' recurse=yes - #when: item.stat.isdir - #with_items: "{{ minimize_access_directories.results }}" - #no_log: True +- name: Minimize access + file: path='{{ item.stat.path }}' mode='go-w' recurse=yes + when: item.stat.isdir + with_items: "{{ minimize_access_directories.results }}" + no_log: True -#- name: Change shadow ownership to root and mode to 0600 - #file: dest='/etc/shadow' owner=root group=root mode=0600 +- name: Change shadow ownership to root and mode to 0600 + file: dest='/etc/shadow' owner=root group=root mode=0600 -#- name: change su-binary to only be accessible to user and group root - #file: dest='/bin/su' owner=root group=root mode=0750 +- name: change su-binary to only be accessible to user and group root + file: dest='/bin/su' owner=root group=root mode=0750 -#- name: Collect Use of privileged commands - #shell: > - #/usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' - #args: - #executable: /bin/bash - #register: privileged_programs +- name: Collect Use of privileged commands + shell: > + /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' + args: + executable: /bin/bash + register: privileged_programs -## Rsyslog +# Rsyslog -#- name: Rsyslog configured - #template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf - #notify: - #- restart rsyslog +- name: Rsyslog configured + template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf + notify: + - restart rsyslog -#- name: Rsyslog CIS configured - #template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 - #notify: - #- restart rsyslog +- name: Rsyslog CIS configured + template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 + notify: + - restart rsyslog -#- name: Enable services - #service: name=rsyslog enabled=yes +- name: Enable services + service: name=rsyslog enabled=yes -## Core dumps +# Core dumps -#- name: Restrict core dumps (with PAM) - #lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present +- name: Restrict core dumps (with PAM) + lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present -#- name: Restrict core dumps (with sysctl) - #sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present +- name: Restrict core dumps (with sysctl) + sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present -## Kernel fixes +# Kernel fixes -#- name: Disable Source Routed Packet Acceptance - #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - #with_items: - #- net.ipv4.conf.all.accept_source_route - #- net.ipv4.conf.default.accept_source_route - #notify: - #- flush routing cache +- name: Disable Source Routed Packet Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_source_route + - net.ipv4.conf.default.accept_source_route + notify: + - flush routing cache -#- name: Disable ICMP Redirect Acceptance - #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - #with_items: - #- net.ipv4.conf.all.accept_redirects - #- net.ipv4.conf.default.accept_redirects +- name: Disable ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_redirects + - net.ipv4.conf.default.accept_redirects -#- name: Disable Secure ICMP Redirect Acceptance - #sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - #with_items: - #- net.ipv4.conf.all.secure_redirects - #- net.ipv4.conf.default.secure_redirects - #notify: - #- flush routing cache +- name: Disable Secure ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.secure_redirects + - net.ipv4.conf.default.secure_redirects + notify: + - flush routing cache -#- name: Enable Bad Error Message Protection - #sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - #notify: - #- flush routing cache +- name: Enable Bad Error Message Protection + sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + notify: + - flush routing cache -#- name: Enable RFC-recommended Source Route Validation - #sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - #with_items: - #- net.ipv4.conf.all.rp_filter - #- net.ipv4.conf.default.rp_filter - #notify: - #- flush routing cache +- name: Enable RFC-recommended Source Route Validation + sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.rp_filter + - net.ipv4.conf.default.rp_filter + notify: + - flush routing cache -#- name: Enable packet forwarding for IPv4 - #sysctl: name=net.ipv4.ip_forward value=1 +- name: Enable packet forwarding for IPv4 + sysctl: name=net.ipv4.ip_forward value=1 -#- name: Enable packet forwarding for IPv6 - #sysctl: name=net.ipv6.conf.all.forwarding value=1 +- name: Enable packet forwarding for IPv6 + sysctl: name=net.ipv6.conf.all.forwarding value=1 -#- name: Do not send ICMP redirects (we are not a router) - #sysctl: name=net.ipv4.conf.all.send_redirects value=0 +- name: Do not send ICMP redirects (we are not a router) + sysctl: name=net.ipv4.conf.all.send_redirects value=0 - name: Iptables configured template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 From 1a6e3775d1cc5c82a31d068965ef420dcbeec421 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 16:36:33 +0300 Subject: [PATCH 063/769] ULA IPv6 --- config.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index cb161eec..f2374d16 100644 --- a/config.cfg +++ b/config.cfg @@ -8,7 +8,8 @@ easyrsa_cert_expire: 3650 easyrsa_reinit_existent: False vpn_network: 10.19.48.0/24 -vpn_network_ipv6: 'fd9d:bc11:4021:69ce::/64' +vpn_network_ipv6: 'fd9d:bc11:4020::/48' +# https://www.sixxs.net/tools/whois/?fd9d:bc11:4020::/48 server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" From 4b2ae71ffe6819b2d4c9aee5464a719d79291820 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 16:49:34 +0300 Subject: [PATCH 064/769] Tighten the dnsmasq AppArmor policy #62 --- .../templates/usr.sbin.dnsmasq.j2 | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 index 9b2c34bd..9afbb34a 100644 --- a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 +++ b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 @@ -1,40 +1,18 @@ -# ------------------------------------------------------------------ -# -# Copyright (C) 2009 John Dong -# Copyright (C) 2010 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License published by the Free Software Foundation. -# -# ------------------------------------------------------------------ - -@{TFTP_DIR}=/var/tftp /srv/tftpboot - #include /usr/sbin/dnsmasq { #include - #include #include capability net_bind_service, capability setgid, capability setuid, capability dac_override, - capability net_admin, # for DHCP server - capability net_raw, # for DHCP server ping checks network inet raw, - signal (receive) peer=/usr/sbin/libvirtd, - ptrace (readby) peer=/usr/sbin/libvirtd, - /etc/dnsmasq.conf r, /etc/dnsmasq.d/ r, /etc/dnsmasq.d/* r, - /etc/ethers r, - /etc/NetworkManager/dnsmasq.d/ r, - /etc/NetworkManager/dnsmasq.d/* r, /etc/block.hosts r, /usr/sbin/dnsmasq mr, @@ -44,25 +22,4 @@ /{,var/}run/dnsmasq/ r, /{,var/}run/dnsmasq/* rw, - /var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage - - # for the read-only TFTP server - @{TFTP_DIR}/ r, - @{TFTP_DIR}/** r, - - # libvirt config, lease and hosts files for dnsmasq - /var/lib/libvirt/dnsmasq/ r, - /var/lib/libvirt/dnsmasq/* r, - /var/lib/libvirt/dnsmasq/*.leases rw, - - # libvirt pid files for dnsmasq - /{,var/}run/libvirt/network/ r, - /{,var/}run/libvirt/network/*.pid rw, - - # NetworkManager integration - /{,var/}run/nm-dns-dnsmasq.conf r, - /{,var/}run/sendsigs.omit.d/*dnsmasq.pid w, - /{,var/}run/NetworkManager/dnsmasq.conf r, - /{,var/}run/NetworkManager/dnsmasq.pid w, - } From 3864f8104dea1d464e6697912c13e649878f4d73 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 17:25:06 +0300 Subject: [PATCH 065/769] adblock.sh as an unprivileged user; Store the whitelists in /var/; #64 --- roles/dns_adblocking/tasks/main.yml | 12 +++++++++++- roles/dns_adblocking/templates/adblock.sh | 8 ++++---- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 | 3 ++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index aa72a4ce..27dabe17 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -8,6 +8,9 @@ template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 notify: - restart dnsmasq + +- name: The dnsmasq directory created + file: dest=/var/lib/dnsmasq state=directory mode=755 owner=dnsmasq group=nogroup - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq @@ -21,11 +24,18 @@ template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 - name: Adblock script added to cron - cron: name="Adblock hosts update" minute="10" hour="2" job="/opt/adblock.sh" + cron: + name: Adblock hosts update + minute: 10 + hour: 2 + job: /opt/adblock.sh + user: dnsmasq - name: Update adblock hosts shell: > /opt/adblock.sh + become: true + become_user: dnsmasq - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh index a6a88581..66196490 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh @@ -7,7 +7,7 @@ ENDPOINT_IP6="::" IPV6="Y" #Delete the old block.hosts to make room for the updates -rm -f /etc/block.hosts +rm -f /var/lib/dnsmasq/block.hosts echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) @@ -32,9 +32,9 @@ then #Filter the blacklist, supressing whitelist matches # This is relatively slow =-( echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /etc/block.hosts + egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /var/lib/dnsmasq/block.hosts else - cat /tmp/block.build.before > /etc/block.hosts + cat /tmp/block.build.before > /var/lib/dnsmasq/block.hosts fi if [ "$IPV6" = "Y" ] @@ -42,7 +42,7 @@ then safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') echo 'Adding ipv6 support...' - sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /etc/block.hosts + sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /var/lib/dnsmasq/block.hosts fi service dnsmasq restart diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index d28cfac3..316f11a3 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -130,7 +130,7 @@ bind-interfaces #no-hosts # or if you want it to read another file, as well as /etc/hosts, use # this. -addn-hosts=/etc/block.hosts +addn-hosts=/var/lib/dnsmasq/block.hosts # Set this (and domain: see below) if you want to have a domain # automatically added to simple names in a hosts-file. diff --git a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 index 9afbb34a..cf4a1e4d 100644 --- a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 +++ b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 @@ -13,7 +13,8 @@ /etc/dnsmasq.conf r, /etc/dnsmasq.d/ r, /etc/dnsmasq.d/* r, - /etc/block.hosts r, + /var/lib/dnsmasq/ r, + /var/lib/dnsmasq/block.hosts r, /usr/sbin/dnsmasq mr, From 53f60e33d88bf866e2817184b069310c4aceb199 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sat, 20 Aug 2016 17:45:35 +0300 Subject: [PATCH 066/769] random tmp names #64 --- roles/dns_adblocking/templates/adblock.sh | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh index 66196490..e2bd95fd 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh @@ -1,40 +1,42 @@ #!/bin/sh -#Block ads, malware, etc. +# Block ads, malware, etc.. # Redirect endpoint ENDPOINT_IP4="0.0.0.0" ENDPOINT_IP6="::" IPV6="Y" +TEMP=`mktemp` +TEMP_SORTED=`mktemp` #Delete the old block.hosts to make room for the updates rm -f /var/lib/dnsmasq/block.hosts echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) -wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > /tmp/block.build.list -wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list -wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> /tmp/block.build.list +wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > "$TEMP" +wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" #Add black list, if non-empty -if [ -s "/etc/black.list" ] +if [ -s "/var/lib/dnsmasq/black.list" ] then echo 'Adding blacklist...' - awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /etc/black.list >> /tmp/block.build.list + awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /var/lib/dnsmasq/black.list >> "$TEMP" fi #Sort the download/black lists -awk '{sub(/\r$/,"");print $1,$2}' /tmp/block.build.list|sort -u > /tmp/block.build.before +awk '{sub(/\r$/,"");print $1,$2}' "$TEMP"|sort -u > "$TEMP_SORTED" #Filter (if applicable) -if [ -s "/etc/white.list" ] +if [ -s "/var/lib/dnsmasq/white.list" ] then #Filter the blacklist, supressing whitelist matches # This is relatively slow =-( echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" /etc/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - /tmp/block.build.before > /var/lib/dnsmasq/block.hosts + egrep -v "^[[:space:]]*$" /var/lib/dnsmasq/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > /var/lib/dnsmasq/block.hosts else - cat /tmp/block.build.before > /var/lib/dnsmasq/block.hosts + cat "$TEMP_SORTED" > /var/lib/dnsmasq/block.hosts fi if [ "$IPV6" = "Y" ] From 6c81b86c92cbd7a13dda8bde17c47d0f9965ae29 Mon Sep 17 00:00:00 2001 From: Colin Mahns Date: Sat, 20 Aug 2016 14:40:33 -0400 Subject: [PATCH 067/769] Link to MVPS Hosts file directly http://www.mvps.org/winhelp2002/hosts.txt redirects to http://winhelp2002.mvps.org/hosts.txt automatically, saves a step --- roles/dns_adblocking/templates/adblock.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh index e2bd95fd..daae47eb 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh @@ -13,7 +13,7 @@ rm -f /var/lib/dnsmasq/block.hosts echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) -wget -qO- http://www.mvps.org/winhelp2002/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > "$TEMP" +wget -qO- http://winhelp2002.mvps.org/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > "$TEMP" wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" From 1fbe1b63f8be2fb6c2c5faeb2a88f977eb1bdc33 Mon Sep 17 00:00:00 2001 From: Colin Mahns Date: Sat, 20 Aug 2016 14:48:31 -0400 Subject: [PATCH 068/769] HTTPS for domains that support it hosts-file.net and malwaredomainlist.com has optional TLS, adaway.org forces it server side --- roles/dns_adblocking/templates/adblock.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh index daae47eb..46a55d14 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh @@ -14,9 +14,9 @@ rm -f /var/lib/dnsmasq/block.hosts echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) wget -qO- http://winhelp2002.mvps.org/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > "$TEMP" -wget -qO- "http://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" -wget -qO- http://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" -wget -qO- "http://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +wget -qO- "https://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +wget -qO- https://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +wget -qO- "https://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" #Add black list, if non-empty if [ -s "/var/lib/dnsmasq/black.list" ] From e6090b8245057f34eca354a5c4693a147814d5b2 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 12:51:58 +0300 Subject: [PATCH 069/769] forwarding #61 --- roles/common/tasks/main.yml | 11 ++++++++++- roles/security/tasks/main.yml | 6 ------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index b1e397a2..f96a4b31 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -70,4 +70,13 @@ notify: - restart loopback -- meta: flush_handlers +- meta: flush_handlers + +- name: Enable packet forwarding for IPv4 + sysctl: name="{{ item }}" value=1 + with_items: + - net.ipv4.ip_forward + - net.ipv4.conf.all.forwarding + +- name: Enable packet forwarding for IPv6 + sysctl: name=net.ipv6.conf.all.forwarding value=1 diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 1ef078ab..fdd48183 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -90,12 +90,6 @@ notify: - flush routing cache -- name: Enable packet forwarding for IPv4 - sysctl: name=net.ipv4.ip_forward value=1 - -- name: Enable packet forwarding for IPv6 - sysctl: name=net.ipv6.conf.all.forwarding value=1 - - name: Do not send ICMP redirects (we are not a router) sysctl: name=net.ipv4.conf.all.send_redirects value=0 From c2fee34062ee733b333e48213d5da7104ec793cd Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 13:01:53 +0300 Subject: [PATCH 070/769] fixes #65 --- algo | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 2676faca..8283b3c3 100755 --- a/algo +++ b/algo @@ -1,5 +1,6 @@ #!/usr/bin/env bash +set -e algo_provisioning () { echo -n " @@ -12,7 +13,7 @@ algo_provisioning () { Enter the number of your desired provider : " - read N + read -r N case "$N" in 1) CLOUD="digitalocean" ;; From ba50abce8a786fa6550517ef22d1c17fe963ad2a Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 13:29:53 +0300 Subject: [PATCH 071/769] make local ip changeable #67 --- config.cfg | 3 +++ roles/common/templates/10-loopback-services.cfg.j2 | 2 +- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- roles/proxy/templates/ports.conf.j2 | 6 +++--- roles/proxy/templates/privoxy_config.j2 | 4 ++-- roles/security/templates/rules.v4.j2 | 7 ++----- roles/security/templates/rules.v6.j2 | 3 --- roles/vpn/handlers/main.yml | 2 +- roles/vpn/templates/ipsec.conf.j2 | 8 ++++---- 9 files changed, 17 insertions(+), 20 deletions(-) diff --git a/config.cfg b/config.cfg index f2374d16..0252cc13 100644 --- a/config.cfg +++ b/config.cfg @@ -22,6 +22,9 @@ dns_servers: - 8.8.4.4 - 2001:4860:4860::8888 - 2001:4860:4860::8844 + +# IP address for proxy and local dns resolver +local_service_ip: 172.16.0.1 users: - mr.smith diff --git a/roles/common/templates/10-loopback-services.cfg.j2 b/roles/common/templates/10-loopback-services.cfg.j2 index c5c47e47..09f572de 100644 --- a/roles/common/templates/10-loopback-services.cfg.j2 +++ b/roles/common/templates/10-loopback-services.cfg.j2 @@ -1,6 +1,6 @@ auto lo:100 iface lo:100 inet static - address 172.16.0.1 + address {{ local_service_ip }} netmask 255.255.255.255 iface lo:100 inet6 static diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 316f11a3..69c317e6 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -110,7 +110,7 @@ group=nogroup #except-interface= # Or which to listen on by address (remember to include 127.0.0.1 if # you use this.) -listen-address=172.16.0.1,127.0.0.1,FCAA::1 +listen-address=127.0.0.1,FCAA::1,{{ local_service_ip }} # If you want dnsmasq to provide only DNS service on an interface, # configure it as shown above, and then use the following line to # disable DHCP and TFTP on it. diff --git a/roles/proxy/templates/ports.conf.j2 b/roles/proxy/templates/ports.conf.j2 index 2618436c..eb6be226 100644 --- a/roles/proxy/templates/ports.conf.j2 +++ b/roles/proxy/templates/ports.conf.j2 @@ -2,12 +2,12 @@ # have to change the VirtualHost statement in # /etc/apache2/sites-enabled/000-default.conf -Listen 172.16.0.1:8080 +Listen {{ local_service_ip }}:8080 - Listen 172.16.0.1:443 + Listen {{ local_service_ip }}:443 - Listen 172.16.0.1:443 + Listen {{ local_service_ip }}:443 diff --git a/roles/proxy/templates/privoxy_config.j2 b/roles/proxy/templates/privoxy_config.j2 index dd55f0f3..485734cc 100644 --- a/roles/proxy/templates/privoxy_config.j2 +++ b/roles/proxy/templates/privoxy_config.j2 @@ -781,7 +781,7 @@ logfile logfile # listen-address [::1]:8118 # # -listen-address 172.16.0.1:8118 +listen-address {{ local_service_ip }}:8118 # # 4.2. toggle # ============ @@ -1256,7 +1256,7 @@ enable-proxy-authentication-forwarding 0 # forward / parent-proxy.example.org:8000 # forward ipv6-server.example.org . # forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . -forward / 172.16.0.1:8080 +forward / {{ local_service_ip }}:8080 forward :443 . # # diff --git a/roles/security/templates/rules.v4.j2 b/roles/security/templates/rules.v4.j2 index 46fbe85c..c8dc1ded 100644 --- a/roles/security/templates/rules.v4.j2 +++ b/roles/security/templates/rules.v4.j2 @@ -1,9 +1,6 @@ *nat :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if dns_enabled is defined and dns_enabled == "Y" %} --A PREROUTING -s {{ vpn_network }} -p udp -m udp --dport 53 -j DNAT --to-destination 172.16.0.1:53 -{% endif %} -A POSTROUTING -s {{ vpn_network }} -m policy --pol none --dir out -j MASQUERADE COMMIT *filter @@ -22,8 +19,8 @@ COMMIT # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. --A INPUT -d 172.16.0.1 -p udp --dport 53 -j ACCEPT --A INPUT -d 172.16.0.1 -p tcp -m multiport --dport 8080,8118 -j ACCEPT +-A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT +-A INPUT -d {{ local_service_ip }} -p tcp -m multiport --dport 8080,8118 -j ACCEPT -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP diff --git a/roles/security/templates/rules.v6.j2 b/roles/security/templates/rules.v6.j2 index 14449cf7..a0c38e88 100644 --- a/roles/security/templates/rules.v6.j2 +++ b/roles/security/templates/rules.v6.j2 @@ -1,9 +1,6 @@ *nat :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if dns_enabled is defined and dns_enabled == "Y" %} --A PREROUTING -s {{ vpn_network_ipv6 }} -p udp -m udp --dport 53 -j DNAT --to-destination fcaa::1:53 -{% endif %} -A POSTROUTING -s {{ vpn_network_ipv6 }} -m policy --pol none --dir out -j MASQUERADE COMMIT *filter diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 94ccdfca..203e1ef9 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -13,5 +13,5 @@ - "# Config files and certificates are in the ./configs/ directory. #" - "# Go to https://www.dnsleaktest.com/ after connecting #" - "# and ensure that all your traffic passes through the VPN. #" - - "# Local DNS resolver and Proxy IP address: 172.16.0.1 #" + - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" - "#----------------------------------------------------------------------#" diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index cd00596c..e0bec01d 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -23,10 +23,10 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if service_dns is defined and service_dns == "N" %} - rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} -{% else %} - rightdns=172.16.0.1 +{% if service_dns is defined and service_dns == "Y" %} + rightdns={{ local_service_ip }} +{% else %} + rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} {% endif %} From d41561b4e327acb4f5e3ede2781eaebc3c0c10ba Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 13:30:54 +0300 Subject: [PATCH 072/769] fixes --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 0252cc13..1af7e13a 100644 --- a/config.cfg +++ b/config.cfg @@ -23,7 +23,7 @@ dns_servers: - 2001:4860:4860::8888 - 2001:4860:4860::8844 -# IP address for proxy and local dns resolver +# IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 users: From 71ad2f570e3605e2630d888b793be22d0f308cfb Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 19:57:52 +0300 Subject: [PATCH 073/769] proxy prompts enabled #70 --- digitalocean.yml | 11 ++++++++--- ec2.yml | 9 +++++++-- gce.yml | 9 +++++++-- non-cloud.yml | 10 ++++++++-- roles/cloud-digitalocean/tasks/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 1 + roles/cloud-gce/tasks/main.yml | 1 + 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/digitalocean.yml b/digitalocean.yml index f2f3d4ff..4dbc3c9b 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -50,9 +50,14 @@ private: no - name: "dns_enabled" - prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" default: "Y" private: no + + - name: "proxy_enabled" + prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" + default: "Y" + private: no - name: "auditd_enabled" prompt: "Do you want to use auditd ? (Y or N):\n" @@ -120,9 +125,9 @@ roles: - common - security - - proxy - vpn - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } + - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } handlers: diff --git a/ec2.yml b/ec2.yml index f880d0ff..d88296eb 100644 --- a/ec2.yml +++ b/ec2.yml @@ -58,7 +58,12 @@ private: no - name: "dns_enabled" - prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no + + - name: "proxy_enabled" + prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" default: "Y" private: no @@ -91,7 +96,7 @@ roles: - common - security - - proxy - vpn + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/gce.yml b/gce.yml index 3c8e9f73..6f1b4525 100644 --- a/gce.yml +++ b/gce.yml @@ -54,7 +54,12 @@ private: no - name: "dns_enabled" - prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no + + - name: "proxy_enabled" + prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" default: "Y" private: no @@ -87,7 +92,7 @@ roles: - common - security - - proxy - vpn + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/non-cloud.yml b/non-cloud.yml index 1623a551..0aca4fc1 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -15,7 +15,12 @@ private: no - name: "dns_enabled" - prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" + default: "Y" + private: no + + - name: "proxy_enabled" + prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" default: "Y" private: no @@ -41,6 +46,7 @@ ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" + proxy_enabled: "{{ proxy_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" IP_subject: "{{ IP_subject }}" @@ -63,7 +69,7 @@ roles: - common - security - - proxy - vpn + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 7bdee8be..a7d52732 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -32,6 +32,7 @@ do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" dns_enabled: "{{ dns_enabled }}" + proxy_enabled: "{{ proxy_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: digitalocean diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index dd657534..13b897e9 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -70,6 +70,7 @@ ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" + proxy_enabled: "{{ proxy_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 4ab0ee23..4bddb274 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -21,6 +21,7 @@ ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" + proxy_enabled: "{{ proxy_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: gce From c51fe5dac04ecd4c21927d6a0c8f1248834261ca Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 21 Aug 2016 20:32:31 +0300 Subject: [PATCH 074/769] run charon as non-root user #66 --- roles/security/templates/rules.v6.j2 | 2 +- roles/vpn/tasks/main.yml | 24 ++++++++++++++++++++---- roles/vpn/templates/strongswan.conf.j2 | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 roles/vpn/templates/strongswan.conf.j2 diff --git a/roles/security/templates/rules.v6.j2 b/roles/security/templates/rules.v6.j2 index a0c38e88..e491fec5 100644 --- a/roles/security/templates/rules.v6.j2 +++ b/roles/security/templates/rules.v6.j2 @@ -6,7 +6,7 @@ COMMIT *filter :INPUT DROP [0:0] :FORWARD DROP [0:0] -:OUTPUT DROP [0:0] +:OUTPUT ACCEPT [0:0] :ICMPV6-CHECK - [0:0] :ICMPV6-CHECK-LOG - [0:0] -A INPUT -i lo -j ACCEPT diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index b21f52ba..1b41d0ad 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -19,6 +19,17 @@ - apparmor - strongswan - netfilter-persistent + +- name: Ensure that the strongswan group exist + group: name=strongswan state=present + +- name: Ensure that the strongswan user exist + user: name=strongswan group=strongswan state=present + +- name: Setup the strongswan.conf file from our template + template: src=strongswan.conf.j2 dest=/etc/strongswan.conf owner=root group=root mode=0644 + notify: + - restart strongswan - name: Setup the ipsec.conf file from our template template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 @@ -26,7 +37,7 @@ - restart strongswan - name: Setup the ipsec.secrets file - template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=root group=root mode=0600 + template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=strongswan group=root mode=0600 notify: - restart strongswan @@ -87,17 +98,17 @@ with_items: "{{ users }}" - name: Copy the CA cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=root group=root mode=0600 + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=strongswan group=root mode=0600 notify: - restart strongswan - name: Copy the server cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=root group=root mode=0600 + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=strongswan group=root mode=0600 notify: - restart strongswan - name: Copy the server key to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=root group=root mode=0600 + copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=strongswan group=root mode=0600 notify: - restart strongswan @@ -126,6 +137,11 @@ - name: Fetch users mobileconfig fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" + +- name: Restrict permissions + file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root + with_items: + - /etc/ipsec.d/private - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 new file mode 100644 index 00000000..4eab82fd --- /dev/null +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -0,0 +1,16 @@ +# strongswan.conf - strongSwan configuration file +# +# Refer to the strongswan.conf(5) manpage for details +# +# Configuration changes should be made in the included files + +charon { + load_modular = yes + plugins { + include strongswan.d/charon/*.conf + } + user = strongswan + group = strongswan +} + +include strongswan.d/*.conf From 256cbe230ed2e4b52c43e266cd904d1d313278dd Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 22 Aug 2016 13:26:00 -0400 Subject: [PATCH 075/769] Add info about TKM to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f8612bb..c2a5acb2 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Raccoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maint ### Why aren't you using a memory-safe or verified IKE daemon? -I would, but I don't know of any. If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to StrongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. +I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to StrongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. ### Why aren't you using OpenVPN? From 09c39627d9abc870c151faadffdb502d08abde51 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Mon, 22 Aug 2016 23:01:43 +0300 Subject: [PATCH 076/769] Memory limits #63 --- config.cfg | 4 ++++ roles/common/tasks/main.yml | 1 + roles/vpn/handlers/main.yml | 2 +- roles/vpn/tasks/main.yml | 22 +++++++++++++------ .../templates/100-CustomLimitations.conf.j2 | 2 ++ 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 roles/vpn/templates/100-CustomLimitations.conf.j2 diff --git a/config.cfg b/config.cfg index 1af7e13a..e380970c 100644 --- a/config.cfg +++ b/config.cfg @@ -7,12 +7,16 @@ easyrsa_cert_expire: 3650 # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False +# Strongswan cgroup limitations +ipsec_memory_limit: 67108864 + vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' # https://www.sixxs.net/tools/whois/?fd9d:bc11:4020::/48 server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" + # Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) service_dns: True diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index f96a4b31..9752cc8d 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -53,6 +53,7 @@ - sendmail - unattended-upgrades - iptables-persistent + - cgroup-tools - name: Configure unattended-upgrades template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 203e1ef9..8abb32cb 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart strongswan - service: name=strongswan state=restarted + systemd: name=strongswan state=restarted daemon_reload=yes - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 1b41d0ad..959861ff 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -11,7 +11,7 @@ - /usr/lib/ipsec/lookip - /usr/lib/ipsec/stroke notify: - - restart apparmor + - restart apparmor - name: Enable services service: name={{ item }} enabled=yes @@ -26,20 +26,28 @@ - name: Ensure that the strongswan user exist user: name=strongswan group=strongswan state=present +- name: Ensure that the strongswan service directory exist + file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root + +- name: Setup the cgroup limitations for the ipsec daemon + template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf + notify: + - restart strongswan + - name: Setup the strongswan.conf file from our template template: src=strongswan.conf.j2 dest=/etc/strongswan.conf owner=root group=root mode=0644 notify: - - restart strongswan + - restart strongswan - name: Setup the ipsec.conf file from our template template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 notify: - - restart strongswan + - restart strongswan - name: Setup the ipsec.secrets file template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=strongswan group=root mode=0600 notify: - - restart strongswan + - restart strongswan - name: Fetch easy-rsa-ipsec from git git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git version=ed4de10d7ce0726357fb1bb4729f8eb440c06e2b dest="{{ easyrsa_dir }}" @@ -100,17 +108,17 @@ - name: Copy the CA cert to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=strongswan group=root mode=0600 notify: - - restart strongswan + - restart strongswan - name: Copy the server cert to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=strongswan group=root mode=0600 notify: - - restart strongswan + - restart strongswan - name: Copy the server key to the strongswan directory copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=strongswan group=root mode=0600 notify: - - restart strongswan + - restart strongswan - name: Register p12 PayloadContent shell: > diff --git a/roles/vpn/templates/100-CustomLimitations.conf.j2 b/roles/vpn/templates/100-CustomLimitations.conf.j2 new file mode 100644 index 00000000..b855e4ed --- /dev/null +++ b/roles/vpn/templates/100-CustomLimitations.conf.j2 @@ -0,0 +1,2 @@ +[Service] +MemoryLimit={{ ipsec_memory_limit }} From bf83ea7f59e1aec3e543fb7de19c1026fa6980d3 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Mon, 22 Aug 2016 23:05:56 +0300 Subject: [PATCH 077/769] systemd module requirements to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2a5acb2..a8df6e46 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere ### Requirements -* ansible >= 2.1.0 +* ansible >= 2.2 * python >= 2.6 * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) * [boto](https://github.com/boto/boto) From 3e9c712068cefca4a895999b8dab06f41501db65 Mon Sep 17 00:00:00 2001 From: Ryan Stortz Date: Mon, 22 Aug 2016 20:46:28 -0400 Subject: [PATCH 078/769] Update README.md Grammar fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8df6e46..6c45e108 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ### Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to update and maintain the software themselves. OpenVPN depends on the security of the [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due to [past security incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to update and maintain the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due to [past security incidents](https://www.exploit-db.com/exploits/34879/). ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? From 50f43dc601c32624d283f4d690fc213b64b93700 Mon Sep 17 00:00:00 2001 From: Defunct Date: Tue, 23 Aug 2016 02:02:57 +0000 Subject: [PATCH 079/769] revert systemd changes (2.2 only), identation normalization; --- roles/dns_adblocking/tasks/main.yml | 4 ++-- roles/proxy/tasks/main.yml | 10 +++++----- roles/security/tasks/main.yml | 2 +- roles/vpn/handlers/main.yml | 2 +- roles/vpn/tasks/main.yml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 27dabe17..a74e455b 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -7,7 +7,7 @@ - name: Dnsmasq profile for apparmor configured template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 notify: - - restart dnsmasq + - restart dnsmasq - name: The dnsmasq directory created file: dest=/var/lib/dnsmasq state=directory mode=755 owner=dnsmasq group=nogroup @@ -18,7 +18,7 @@ - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf notify: - - restart dnsmasq + - restart dnsmasq - name: Adblock script created template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index ae062cf3..e45124cc 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -12,7 +12,7 @@ - name: Privoxy profile for apparmor configured template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 notify: - - restart privoxy + - restart privoxy - name: Enforce the privoxy AppArmor policy shell: aa-enforce usr.sbin.privoxy @@ -36,7 +36,7 @@ - name: PageSpeed configured template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf notify: - - restart apache2 + - restart apache2 - name: Modules enabled apache2_module: state=present name="{{ item }}" @@ -48,14 +48,14 @@ - proxy_html - rewrite notify: - - restart apache2 + - restart apache2 - name: VirtualHost configured for the PageSpeed module template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf notify: - - restart apache2 + - restart apache2 - name: Apache ports configured template: src=ports.conf.j2 dest=/etc/apache2/ports.conf notify: - - restart apache2 + - restart apache2 diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index fdd48183..10d31eb3 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -99,4 +99,4 @@ - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: - - restart iptables + - restart iptables diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 8abb32cb..0a527a43 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart strongswan - systemd: name=strongswan state=restarted daemon_reload=yes + service: name=strongswan state=restarted daemon_reload=yes - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 959861ff..7957e422 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -75,7 +75,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/ca_initialized' notify: - - restart strongswan + - restart strongswan - name: Build the server pair shell: > @@ -85,7 +85,7 @@ chdir: '{{ easyrsa_dir }}/easyrsa3/' creates: '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' notify: - - restart strongswan + - restart strongswan - name: Build the client's pair shell: > From 468d5af23d4f4a385527c68fdc277365583c2520 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Tue, 23 Aug 2016 09:00:32 +0300 Subject: [PATCH 080/769] service fixes --- README.md | 2 +- roles/vpn/handlers/main.yml | 3 +++ roles/vpn/tasks/main.yml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c45e108..d5c878a0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere ### Requirements -* ansible >= 2.2 +* ansible >= 2.1 * python >= 2.6 * [dopy=0.3.5](https://github.com/Wiredcraft/dopy) * [boto](https://github.com/boto/boto) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 0a527a43..a9d4d72e 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,8 @@ - name: restart strongswan service: name=strongswan state=restarted daemon_reload=yes + +- name: daemon-reload + shell: systemctl systemctl daemon-reload - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 7957e422..0dd10ea6 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -32,6 +32,7 @@ - name: Setup the cgroup limitations for the ipsec daemon template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf notify: + - daemon-reload - restart strongswan - name: Setup the strongswan.conf file from our template From 5ecd23c59ccf0d1c1e76c8cbc4a4cba68828bf09 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Tue, 23 Aug 2016 09:01:07 +0300 Subject: [PATCH 081/769] type --- roles/vpn/handlers/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index a9d4d72e..0ed78a34 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -2,7 +2,7 @@ service: name=strongswan state=restarted daemon_reload=yes - name: daemon-reload - shell: systemctl systemctl daemon-reload + shell: systemctl daemon-reload - name: restart apparmor service: name=apparmor state=restarted From 19797bc020074022d494fdd0af7a738f108eb8db Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 Aug 2016 16:10:42 +0300 Subject: [PATCH 082/769] CPU and memory limitations of the services #63 --- config.cfg | 3 --- roles/dns_adblocking/tasks/main.yml | 11 ++++++++++ .../templates/100-CustomLimitations.conf.j2 | 4 ++++ roles/proxy/handlers/main.yml | 3 +++ roles/proxy/tasks/main.yml | 22 +++++++++++++++++++ .../apache2_100-CustomLimitations.conf.j2 | 4 ++++ .../privoxy_100-CustomLimitations.conf.j2 | 4 ++++ roles/vpn/tasks/main.yml | 2 ++ .../templates/100-CustomLimitations.conf.j2 | 2 +- 9 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 create mode 100644 roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 create mode 100644 roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 diff --git a/config.cfg b/config.cfg index e380970c..7f522700 100644 --- a/config.cfg +++ b/config.cfg @@ -7,9 +7,6 @@ easyrsa_cert_expire: 3650 # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False -# Strongswan cgroup limitations -ipsec_memory_limit: 67108864 - vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' # https://www.sixxs.net/tools/whois/?fd9d:bc11:4020::/48 diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index a74e455b..df0fc373 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -15,6 +15,17 @@ - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq +- name: Ensure that the dnsmasq service directory exist + file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root + +- name: Setup the cgroup limitations for the ipsec daemon + template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf + notify: + - daemon-reload + - restart dnsmasq + +- meta: flush_handlers + - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf notify: diff --git a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 new file mode 100644 index 00000000..98cbbddb --- /dev/null +++ b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 @@ -0,0 +1,4 @@ +[Service] +MemoryLimit=16777216 +CPUAccounting=true +CPUQuota=5% diff --git a/roles/proxy/handlers/main.yml b/roles/proxy/handlers/main.yml index 269a0ff8..bea23c72 100644 --- a/roles/proxy/handlers/main.yml +++ b/roles/proxy/handlers/main.yml @@ -1,5 +1,8 @@ - name: restart privoxy service: name=privoxy state=restarted + +- name: daemon-reload + shell: systemctl daemon-reload - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index e45124cc..1157a971 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -16,6 +16,17 @@ - name: Enforce the privoxy AppArmor policy shell: aa-enforce usr.sbin.privoxy + +- name: Ensure that the privoxy service directory exist + file: path=/etc/systemd/system/privoxy.service.d/ state=directory mode=0755 owner=root group=root + +- name: Setup the cgroup limitations for the privoxy daemon + template: src=privoxy_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/privoxy.service.d/100-CustomLimitations.conf + notify: + - daemon-reload + - restart privoxy + +- meta: flush_handlers - name: Privoxy enabled and started service: name=privoxy state=started enabled=yes @@ -59,3 +70,14 @@ template: src=ports.conf.j2 dest=/etc/apache2/ports.conf notify: - restart apache2 + +- name: Ensure that the apache2 service directory exist + file: path=/etc/systemd/system/apache2.service.d/ state=directory mode=0755 owner=root group=root + +- name: Setup the cgroup limitations for the apache2 daemon + template: src=apache2_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/apache2.service.d/100-CustomLimitations.conf + notify: + - daemon-reload + - restart apache2 + +- meta: flush_handlers diff --git a/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 b/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 new file mode 100644 index 00000000..5e9774ef --- /dev/null +++ b/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 @@ -0,0 +1,4 @@ +[Service] +MemoryLimit=134217728 +CPUAccounting=true +CPUQuota=15% diff --git a/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 b/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 new file mode 100644 index 00000000..cd9b628c --- /dev/null +++ b/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 @@ -0,0 +1,4 @@ +[Service] +MemoryLimit=33554432 +CPUAccounting=true +CPUQuota=15% diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 0dd10ea6..1592db4a 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -34,6 +34,8 @@ notify: - daemon-reload - restart strongswan + +- meta: flush_handlers - name: Setup the strongswan.conf file from our template template: src=strongswan.conf.j2 dest=/etc/strongswan.conf owner=root group=root mode=0644 diff --git a/roles/vpn/templates/100-CustomLimitations.conf.j2 b/roles/vpn/templates/100-CustomLimitations.conf.j2 index b855e4ed..d7430af1 100644 --- a/roles/vpn/templates/100-CustomLimitations.conf.j2 +++ b/roles/vpn/templates/100-CustomLimitations.conf.j2 @@ -1,2 +1,2 @@ [Service] -MemoryLimit={{ ipsec_memory_limit }} +MemoryLimit=16777216 From 1dcfe180551cb49b9a8037e6f60c2e9d592bd015 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 Aug 2016 16:51:06 +0300 Subject: [PATCH 083/769] SSH tunneling role #77 --- digitalocean.yml | 6 + ec2.yml | 8 +- gce.yml | 6 + non-cloud.yml | 7 ++ roles/cloud-digitalocean/tasks/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 1 + roles/cloud-gce/tasks/main.yml | 1 + roles/common/templates/sshd_config.j2 | 143 +++++++++--------------- roles/ssh_tunneling/handlers/main.yml | 2 + roles/ssh_tunneling/tasks/main.yml | 21 ++++ 10 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 roles/ssh_tunneling/handlers/main.yml create mode 100644 roles/ssh_tunneling/tasks/main.yml diff --git a/digitalocean.yml b/digitalocean.yml index 4dbc3c9b..7a7e40a8 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -64,6 +64,11 @@ default: "Y" private: no + - name: "ssh_tunneling_enabled" + prompt: "Do you want to use SSH tunneling ? (Y or N):\n" + default: "Y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" @@ -129,6 +134,7 @@ - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } handlers: - name: reload eth0 diff --git a/ec2.yml b/ec2.yml index d88296eb..c9060311 100644 --- a/ec2.yml +++ b/ec2.yml @@ -71,7 +71,12 @@ prompt: "Do you want to use auditd ? (Y or N):\n" default: "Y" private: no - + + - name: "ssh_tunneling_enabled" + prompt: "Do you want to use SSH tunneling ? (Y or N):\n" + default: "Y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" @@ -100,3 +105,4 @@ - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } diff --git a/gce.yml b/gce.yml index 6f1b4525..ff1c5e9e 100644 --- a/gce.yml +++ b/gce.yml @@ -68,6 +68,11 @@ default: "Y" private: no + - name: "ssh_tunneling_enabled" + prompt: "Do you want to use SSH tunneling ? (Y or N):\n" + default: "Y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" @@ -96,3 +101,4 @@ - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } diff --git a/non-cloud.yml b/non-cloud.yml index 0aca4fc1..4ed42dfa 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -29,6 +29,11 @@ default: "Y" private: no + - name: "ssh_tunneling_enabled" + prompt: "Do you want to use SSH tunneling ? (Y or N):\n" + default: "Y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter the password for p12 certificates:\n" default: "vpn" @@ -47,6 +52,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" IP_subject: "{{ IP_subject }}" @@ -73,3 +79,4 @@ - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index a7d52732..73e5c34d 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -33,6 +33,7 @@ do_droplet_id: "{{ do.droplet.id }}" dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: digitalocean diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 13b897e9..cb211897 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -71,6 +71,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 4bddb274..661d9cba 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -22,6 +22,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: gce diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index d17d9f67..af66436c 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -1,106 +1,71 @@ -# Package generated configuration file -# See the sshd_config(5) manpage for details - -# What ports, IPs and protocols we listen for Port 22 - -# Use these options to restrict which interfaces/protocols sshd will bind to -#ListenAddress :: -#ListenAddress 0.0.0.0 +# ListenAddress :: +# ListenAddress 0.0.0.0 Protocol 2 -# HostKeys for protocol version 2 -#HostKey /etc/ssh/ssh_host_rsa_key -#HostKey /etc/ssh/ssh_host_dsa_key -HostKey /etc/ssh/ssh_host_ecdsa_key -HostKey /etc/ssh/ssh_host_ed25519_key +# LogLevel VERBOSE logs user's key fingerprint on login. +# Needed to have a clear audit log of which keys were used to log in. +SyslogFacility AUTH +LogLevel VERBOSE -# Use kernel sandbox mechanisms where possible in unprivilegied processes +# Use kernel sandbox mechanisms where possible # Systrace on OpenBSD, Seccomp on Linux, seatbelt on MacOSX/Darwin, rlimit elsewhere. UsePrivilegeSeparation sandbox -# Lifetime and size of ephemeral version 1 server key -KeyRegenerationInterval 3600 -ServerKeyBits 1024 +# Handy for keeping network connections alive +TCPKeepAlive yes +ClientAliveInterval 120 -# Logging -SyslogFacility AUTH -LogLevel INFO - -# Authentication: -LoginGraceTime 120 +# Authentication +UsePAM yes PermitRootLogin without-password StrictModes yes - -RSAAuthentication no PubkeyAuthentication yes -#AuthorizedKeysFile %h/.ssh/authorized_keys - -# Don't read the user's ~/.rhosts and ~/.shosts files -IgnoreRhosts yes - -# For this to work you will also need host keys in /etc/ssh_known_hosts -RhostsRSAAuthentication no - -# similar for protocol version 2 -HostbasedAuthentication no - -# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication -#IgnoreUserKnownHosts yes - -# To enable empty passwords, change to yes (NOT RECOMMENDED) -PermitEmptyPasswords no - -# Change to yes to enable challenge-response passwords (beware issues with -# some PAM modules and threads) -ChallengeResponseAuthentication no - -# Change to no to disable tunnelled clear text passwords -PasswordAuthentication no - -# Kerberos options -#KerberosAuthentication no -#KerberosGetAFSToken no -#KerberosOrLocalPasswd yes -#KerberosTicketCleanup yes - -# GSSAPI options -#GSSAPIAuthentication no -#GSSAPICleanupCredentials yes - -X11Forwarding no -X11DisplayOffset 10 -PrintMotd no -PrintLastLog yes -TCPKeepAlive yes -#UseLogin no - -#MaxStartups 10:30:60 -#Banner /etc/issue.net - -# Allow client to pass locale environment variables AcceptEnv LANG LC_* -# Subsystem sftp /usr/lib/openssh/sftp-server - -# Set this to 'yes' to enable PAM authentication, account processing, -# and session processing. If this is enabled, PAM authentication will -# be allowed through the ChallengeResponseAuthentication and -# PasswordAuthentication. Depending on your PAM configuration, -# PAM authentication via ChallengeResponseAuthentication may bypass -# the setting of "PermitRootLogin yes -# If you just want the PAM account and session checks to run without -# PAM authentication, then enable this but set PasswordAuthentication -# and ChallengeResponseAuthentication to 'no'. -UsePAM yes - -# Added by DigitalOcean build process -ClientAliveInterval 120 -ClientAliveCountMax 2 +# Turn off a lot of features +AllowAgentForwarding no +IgnoreRhosts yes +RhostsRSAAuthentication no +RSAAuthentication no +HostbasedAuthentication no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +PasswordAuthentication no UseDNS no +X11Forwarding no + +# Do not enable sftp +# If you DO enable it, use this line to log which files sftp users read/write +# Subsystem sftp /usr/lib/ssh/sftp-server -f AUTHPRIV -l INFO + +# This makes ansible faster +PrintMotd no +PrintLastLog yes + +# Use only modern host keys +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key + +# Use only modern ciphers +KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com -KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 -# Password based logins are disabled - only public key based logins are allowed. -AuthenticationMethods publickey +### + +# TODO: I haven't seen anyone review these yet +# HostKeyAlgorithms ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 + +# TODO: I haven't seen anyone review these yet +# PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 + +# TODO: I think we want to enable tunnels but disable stream local fowarding? +# PermitTunnel yes +# AllowStreamLocalForwarding no + +{% if ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" %} +Match Group algo + AllowTcpForwarding remote + AllowStreamLocalForwarding no +{% endif %} diff --git a/roles/ssh_tunneling/handlers/main.yml b/roles/ssh_tunneling/handlers/main.yml new file mode 100644 index 00000000..276ebfe6 --- /dev/null +++ b/roles/ssh_tunneling/handlers/main.yml @@ -0,0 +1,2 @@ +- name: restart ssh + service: name=ssh state=restarted diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml new file mode 100644 index 00000000..7d87c7e8 --- /dev/null +++ b/roles/ssh_tunneling/tasks/main.yml @@ -0,0 +1,21 @@ +--- + +- name: Ensure that the algo group exist + group: name=algo state=present + +- name: Ensure that the jail directory exist + file: path=/var/jail/ state=directory mode=0755 owner=root group=root + +- name: Ensure that the SSH users exist + user: + name: "{{ item }}" + group: algo + home: '/var/jail/{{ item }}' + createhome: yes + generate_ssh_key: yes + shell: /bin/false + ssh_key_type: ecdsa + ssh_key_bits: 521 + ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' + state: present + with_items: "{{ users }}" From 2fcc3600fdb2db2d9ff7e3c4901d0774be8ff58a Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 23 Aug 2016 17:03:27 -0400 Subject: [PATCH 084/769] Disable features in the Match block vs main config --- roles/common/templates/sshd_config.j2 | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index af66436c..8c08f0f3 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -24,7 +24,6 @@ PubkeyAuthentication yes AcceptEnv LANG LC_* # Turn off a lot of features -AllowAgentForwarding no IgnoreRhosts yes RhostsRSAAuthentication no RSAAuthentication no @@ -33,7 +32,6 @@ PermitEmptyPasswords no ChallengeResponseAuthentication no PasswordAuthentication no UseDNS no -X11Forwarding no # Do not enable sftp # If you DO enable it, use this line to log which files sftp users read/write @@ -51,21 +49,16 @@ HostKey /etc/ssh/ssh_host_ed25519_key KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com - -### - # TODO: I haven't seen anyone review these yet # HostKeyAlgorithms ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 - # TODO: I haven't seen anyone review these yet # PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 -# TODO: I think we want to enable tunnels but disable stream local fowarding? -# PermitTunnel yes -# AllowStreamLocalForwarding no - {% if ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" %} Match Group algo AllowTcpForwarding remote + AllowAgentForwarding no AllowStreamLocalForwarding no + PermitTunnel no + X11Forwarding no {% endif %} From b29f1ab226e9e00f4100eb7dfd9161d5c3a9db01 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 24 Aug 2016 10:03:19 +0300 Subject: [PATCH 085/769] service fixed #78 --- roles/vpn/handlers/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 0ed78a34..c5dcdc9e 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart strongswan - service: name=strongswan state=restarted daemon_reload=yes + service: name=strongswan state=restarted - name: daemon-reload shell: systemctl daemon-reload From 809b62cd338cade274583eae003c50cd5301a831 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 24 Aug 2016 09:03:29 +0200 Subject: [PATCH 086/769] daemon_reload is an option for systemd, not service --- roles/vpn/handlers/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 0ed78a34..c5dcdc9e 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart strongswan - service: name=strongswan state=restarted daemon_reload=yes + service: name=strongswan state=restarted - name: daemon-reload shell: systemctl daemon-reload From 1f8e33774e7b1f8cd4fc5f6ebe4f008b6e163d27 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 24 Aug 2016 10:03:51 +0300 Subject: [PATCH 087/769] service fixed #78 --- roles/vpn/handlers/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 0ed78a34..c5dcdc9e 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart strongswan - service: name=strongswan state=restarted daemon_reload=yes + service: name=strongswan state=restarted - name: daemon-reload shell: systemctl daemon-reload From 27421070b9920c19e81705536425eb2e50ec6eb4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 24 Aug 2016 09:22:04 +0200 Subject: [PATCH 088/769] linting --- digitalocean.yml | 39 +++++----- ec2.yml | 113 ++++++++++++++-------------- gce.yml | 33 ++++---- non-cloud.yml | 77 +++++++++---------- roles/common/tasks/main.yml | 4 +- roles/dns_adblocking/tasks/main.yml | 16 ++-- roles/proxy/handlers/main.yml | 4 +- roles/proxy/tasks/main.yml | 20 ++--- roles/security/handlers/main.yml | 4 +- roles/security/tasks/main.yml | 2 +- roles/ssh_tunneling/tasks/main.yml | 10 +-- roles/vpn/handlers/main.yml | 5 ++ roles/vpn/tasks/main.yml | 24 +++--- 13 files changed, 179 insertions(+), 172 deletions(-) diff --git a/digitalocean.yml b/digitalocean.yml index 7a7e40a8..7d6ac8e6 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -1,3 +1,4 @@ +# vim:ft=ansible: - name: Configure the server and install required software hosts: localhost @@ -50,29 +51,29 @@ private: no - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" - default: "Y" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" + default: "y" private: no - + - name: "proxy_enabled" - prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" - default: "Y" - private: no + prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" + default: "y" + private: no - name: "auditd_enabled" - prompt: "Do you want to use auditd ? (Y or N):\n" - default: "Y" + prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" + default: "y" private: no - + - name: "ssh_tunneling_enabled" - prompt: "Do you want to use SSH tunneling ? (Y or N):\n" - default: "Y" - private: no - + prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + default: "y" + private: no + - name: "easyrsa_p12_export_password" - prompt: "Enter the password for p12 certificates:\n" + prompt: "Enter a password for p12 certificates:\n" default: "vpn" - private: yes + private: yes roles: - cloud-digitalocean @@ -131,10 +132,10 @@ - common - security - vpn - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "Y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } + - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "y" } + - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } handlers: - name: reload eth0 diff --git a/ec2.yml b/ec2.yml index c9060311..891f2676 100644 --- a/ec2.yml +++ b/ec2.yml @@ -21,66 +21,65 @@ "11": "sa-east-1" vars_prompt: + - name: "aws_access_key" + prompt: "Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" + private: yes - - name: "aws_access_key" - prompt: "Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" - private: yes + - name: "aws_secret_key" + prompt: "Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" + private: yes - - name: "aws_secret_key" - prompt: "Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" - private: yes + - name: "region" + prompt: > + What region should the server be located in? + 1. us-east-1 US East (N. Virginia) + 2. us-west-1 US West (N. California) + 3. us-west-2 US West (Oregon) + 4. ap-south-1 Asia Pacific (Mumbai) + 5. ap-northeast-2 Asia Pacific (Seoul) + 6. ap-southeast-1 Asia Pacific (Singapore) + 7. ap-southeast-2 Asia Pacific (Sydney) + 8. ap-northeast-1 Asia Pacific (Tokyo) + 9. eu-central-1 EU (Frankfurt) + 10. eu-west-1 EU (Ireland) + 11. sa-east-1 South America (São Paulo) + default: "1" + private: no - - name: "region" - prompt: > - What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-west-1 US West (N. California) - 3. us-west-2 US West (Oregon) - 4. ap-south-1 Asia Pacific (Mumbai) - 5. ap-northeast-2 Asia Pacific (Seoul) - 6. ap-southeast-1 Asia Pacific (Singapore) - 7. ap-southeast-2 Asia Pacific (Sydney) - 8. ap-northeast-1 Asia Pacific (Tokyo) - 9. eu-central-1 EU (Frankfurt) - 10. eu-west-1 EU (Ireland) - 11. sa-east-1 South America (São Paulo) - default: "1" - private: no + - name: "aws_server_name" + prompt: "Name the vpn server:\n" + default: "algo.local" + private: no - - name: "aws_server_name" - prompt: "Name the vpn server:\n" - default: "algo.local" - private: no + - name: "ssh_public_key" + prompt: "Enter the local path to your SSH public key:\n" + default: "~/.ssh/id_rsa.pub" + private: no - - name: "ssh_public_key" - prompt: "Enter the local path to your SSH public key:\n" - default: "~/.ssh/id_rsa.pub" - private: no + - name: "dns_enabled" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" + default: "y" + private: no - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" - default: "Y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" - default: "Y" - private: no + - name: "proxy_enabled" + prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" + default: "y" + private: no - - name: "auditd_enabled" - prompt: "Do you want to use auditd ? (Y or N):\n" - default: "Y" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want to use SSH tunneling ? (Y or N):\n" - default: "Y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter the password for p12 certificates:\n" - default: "vpn" - private: yes + - name: "auditd_enabled" + prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" + default: "y" + private: no + + - name: "ssh_tunneling_enabled" + prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + default: "y" + private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter a password for p12 certificates:\n" + default: "vpn" + private: yes roles: - cloud-ec2 @@ -102,7 +101,7 @@ - common - security - vpn - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } + - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } diff --git a/gce.yml b/gce.yml index ff1c5e9e..b44ce866 100644 --- a/gce.yml +++ b/gce.yml @@ -1,3 +1,4 @@ +# vim:ft=ansible: - name: Configure the server and install required software hosts: localhost gather_facts: false @@ -54,27 +55,27 @@ private: no - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" - default: "Y" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" + default: "y" private: no - + - name: "proxy_enabled" - prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" - default: "Y" + prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" + default: "y" private: no - name: "auditd_enabled" - prompt: "Do you want to use auditd ? (Y or N):\n" - default: "Y" + prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" + default: "y" private: no - + - name: "ssh_tunneling_enabled" - prompt: "Do you want to use SSH tunneling ? (Y or N):\n" - default: "Y" + prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + default: "y" private: no - + - name: "easyrsa_p12_export_password" - prompt: "Enter the password for p12 certificates:\n" + prompt: "Enter a password for p12 certificates:\n" default: "vpn" private: yes @@ -98,7 +99,7 @@ - common - security - vpn - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } + - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } diff --git a/non-cloud.yml b/non-cloud.yml index 4ed42dfa..a823cca1 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -1,47 +1,48 @@ +# vim:ft=ansible: - hosts: localhost gather_facts: False vars_files: - config.cfg + vars_prompt: + - name: "server_ip" + prompt: "Enter IP address of your server: (use localhost for local installation)\n" + default: localhost + private: no - - name: "server_ip" - prompt: "Enter IP address of your server: (use localhost for local installation)\n" - default: localhost - private: no + - name: "server_user" + prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" + default: "root" + private: no - - name: "server_user" - prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" - default: "root" - private: no + - name: "dns_enabled" + prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" + default: "y" + private: no - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (Y or N):\n" - default: "Y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install a proxy to block ads and decrease traffic usage while surfing? (Y or N):\n" - default: "Y" - private: no + - name: "proxy_enabled" + prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" + default: "y" + private: no - - name: "auditd_enabled" - prompt: "Do you want to use auditd ? (Y or N):\n" - default: "Y" - private: no + - name: "auditd_enabled" + prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" + default: "y" + private: no + + - name: "ssh_tunneling_enabled" + prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + default: "y" + private: no + + - name: "easyrsa_p12_export_password" + prompt: "Enter a password for p12 certificates:\n" + default: "vpn" + private: yes - - name: "ssh_tunneling_enabled" - prompt: "Do you want to use SSH tunneling ? (Y or N):\n" - default: "Y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter the password for p12 certificates:\n" - default: "vpn" - private: yes - - - name: "IP_subject" - prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" - private: no + - name: "IP_subject" + prompt: "Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" + private: no tasks: - name: Add the server to the vpn-host group @@ -76,7 +77,7 @@ - common - security - vpn - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "Y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" } + - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } + - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } + - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } + - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 9752cc8d..dc17b891 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -33,7 +33,7 @@ - name: SSH config template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 notify: - - restart ssh + - restart ssh - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" @@ -70,7 +70,7 @@ lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present notify: - restart loopback - + - meta: flush_handlers - name: Enable packet forwarding for IPv4 diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index df0fc373..a37bf9cd 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -8,24 +8,24 @@ template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 notify: - restart dnsmasq - + - name: The dnsmasq directory created - file: dest=/var/lib/dnsmasq state=directory mode=755 owner=dnsmasq group=nogroup + file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq - name: Ensure that the dnsmasq service directory exist file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root - + - name: Setup the cgroup limitations for the ipsec daemon template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf notify: - - daemon-reload + - daemon-reload - restart dnsmasq - -- meta: flush_handlers - + +- meta: flush_handlers + - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf notify: @@ -35,7 +35,7 @@ template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 - name: Adblock script added to cron - cron: + cron: name: Adblock hosts update minute: 10 hour: 2 diff --git a/roles/proxy/handlers/main.yml b/roles/proxy/handlers/main.yml index bea23c72..a31941ba 100644 --- a/roles/proxy/handlers/main.yml +++ b/roles/proxy/handlers/main.yml @@ -1,8 +1,8 @@ - name: restart privoxy service: name=privoxy state=restarted - + - name: daemon-reload - shell: systemctl daemon-reload + shell: systemctl daemon-reload - name: restart apparmor service: name=apparmor state=restarted diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index 1157a971..81dbcab1 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -16,17 +16,17 @@ - name: Enforce the privoxy AppArmor policy shell: aa-enforce usr.sbin.privoxy - + - name: Ensure that the privoxy service directory exist file: path=/etc/systemd/system/privoxy.service.d/ state=directory mode=0755 owner=root group=root - + - name: Setup the cgroup limitations for the privoxy daemon template: src=privoxy_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/privoxy.service.d/100-CustomLimitations.conf notify: - - daemon-reload + - daemon-reload - restart privoxy - -- meta: flush_handlers + +- meta: flush_handlers - name: Privoxy enabled and started service: name=privoxy state=started enabled=yes @@ -70,14 +70,14 @@ template: src=ports.conf.j2 dest=/etc/apache2/ports.conf notify: - restart apache2 - + - name: Ensure that the apache2 service directory exist file: path=/etc/systemd/system/apache2.service.d/ state=directory mode=0755 owner=root group=root - + - name: Setup the cgroup limitations for the apache2 daemon template: src=apache2_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/apache2.service.d/100-CustomLimitations.conf notify: - - daemon-reload + - daemon-reload - restart apache2 - -- meta: flush_handlers + +- meta: flush_handlers diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index f5fb1c9a..ad1168b0 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,8 +1,8 @@ - name: restart rsyslog service: name=rsyslog state=restarted - + - name: restart iptables service: name=netfilter-persistent state=restarted - + - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 10d31eb3..a5288960 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -97,6 +97,6 @@ template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 with_items: - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } - - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } + - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: - restart iptables diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 7d87c7e8..b78b19b3 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -2,12 +2,12 @@ - name: Ensure that the algo group exist group: name=algo state=present - + - name: Ensure that the jail directory exist - file: path=/var/jail/ state=directory mode=0755 owner=root group=root - + file: path=/var/jail/ state=directory mode=0755 owner=root group=root + - name: Ensure that the SSH users exist - user: + user: name: "{{ item }}" group: algo home: '/var/jail/{{ item }}' @@ -17,5 +17,5 @@ ssh_key_type: ecdsa ssh_key_bits: 521 ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - state: present + state: present with_items: "{{ users }}" diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index c5dcdc9e..3e1a70e9 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -1,6 +1,11 @@ - name: restart strongswan +<<<<<<< Updated upstream service: name=strongswan state=restarted +======= + service: name=strongswan state=restartedo + +>>>>>>> Stashed changes - name: daemon-reload shell: systemctl daemon-reload diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 1592db4a..1fe08b90 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,6 +1,6 @@ - name: Gather Facts setup: - + - name: Install StrongSwan apt: name=strongswan state=latest update_cache=yes @@ -19,28 +19,28 @@ - apparmor - strongswan - netfilter-persistent - + - name: Ensure that the strongswan group exist group: name=strongswan state=present - + - name: Ensure that the strongswan user exist user: name=strongswan group=strongswan state=present - + - name: Ensure that the strongswan service directory exist file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root - + - name: Setup the cgroup limitations for the ipsec daemon template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf notify: - - daemon-reload + - daemon-reload - restart strongswan - -- meta: flush_handlers - + +- meta: flush_handlers + - name: Setup the strongswan.conf file from our template template: src=strongswan.conf.j2 dest=/etc/strongswan.conf owner=root group=root mode=0644 notify: - - restart strongswan + - restart strongswan - name: Setup the ipsec.conf file from our template template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 @@ -148,11 +148,11 @@ - name: Fetch users mobileconfig fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" - + - name: Restrict permissions file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root with_items: - - /etc/ipsec.d/private + - /etc/ipsec.d/private - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes From 3eb16cdc58c0545e23d2702ab81c4a2fe3eb59ed Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 24 Aug 2016 09:27:29 +0200 Subject: [PATCH 089/769] Create CONTRIBUTING.md --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..044bc94e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +### Common Errors + +* Check that you're using at least Ansible 2.1 +* If installing to a local server, try using a fresh install + +### Filing Issues + +* Please review the [FAQ](https://github.com/trailofbits/algo#faq) in the readme + +### Coding Guidelines + +* Please review any Pull Requests with [ansible-lint](https://github.com/willthames/ansible-lint) From dbb7fd0815675c89374aa3683a69fd2bc244011f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 24 Aug 2016 09:30:33 +0200 Subject: [PATCH 090/769] Make config.cfg a little more user-friendly --- config.cfg | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config.cfg b/config.cfg index 7f522700..c8178b58 100644 --- a/config.cfg +++ b/config.cfg @@ -1,5 +1,15 @@ --- +# Add as many users as you want for your VPN server here +users: + - dan + - jack + +# If you're using auditd for monitoring, add an email address to send logs +auditd_action_mail_acct: email@example.com + +### Advanced users only below this line ### + easyrsa_dir: /opt/easy-rsa-ipsec easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 @@ -13,7 +23,6 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" - # Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) service_dns: True @@ -23,15 +32,6 @@ dns_servers: - 8.8.4.4 - 2001:4860:4860::8888 - 2001:4860:4860::8844 - -# IP address for the proxy and the local dns resolver -local_service_ip: 172.16.0.1 -users: - - mr.smith - - mrs.smith - -# -# auditd options -# email for auditd actions: -auditd_action_mail_acct: email@example.com +# IP address for the proxy and the local dns resolver +local_service_ip: 172.16.0.1 From e1416398c8aaac1f08b7676f7eb9e5103cf7aec6 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 24 Aug 2016 09:31:52 +0200 Subject: [PATCH 091/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 044bc94e..9a1001f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,7 @@ -### Common Errors +### Common Issues * Check that you're using at least Ansible 2.1 * If installing to a local server, try using a fresh install - -### Filing Issues - * Please review the [FAQ](https://github.com/trailofbits/algo#faq) in the readme ### Coding Guidelines From c19908c9b119c93a851db71f0cbb23e52590bd08 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 25 Aug 2016 23:03:20 +0300 Subject: [PATCH 092/769] ssh fixes --- .gitignore | 5 +---- digitalocean.yml | 6 +++--- ec2.yml | 6 +++--- gce.yml | 6 +++--- non-cloud.yml | 6 +++--- roles/common/templates/sshd_config.j2 | 2 +- roles/ssh_tunneling/tasks/main.yml | 18 ++++++++++++++++-- 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 7d9d96c8..9df513b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ *.retry -configs/*.mobileconfig -configs/*.p12 -configs/*.crt -configs/*.tmp +configs/* inventory_users diff --git a/digitalocean.yml b/digitalocean.yml index 7d6ac8e6..687bfbcc 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -71,8 +71,8 @@ private: no - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates:\n" - default: "vpn" + prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + default: "vpnpw" private: yes roles: @@ -131,11 +131,11 @@ roles: - common - security - - vpn - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - vpn handlers: - name: reload eth0 diff --git a/ec2.yml b/ec2.yml index 891f2676..a988be6a 100644 --- a/ec2.yml +++ b/ec2.yml @@ -77,8 +77,8 @@ private: no - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates:\n" - default: "vpn" + prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + default: "vpnpw" private: yes roles: @@ -100,8 +100,8 @@ roles: - common - security - - vpn - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - vpn diff --git a/gce.yml b/gce.yml index b44ce866..24a0cb95 100644 --- a/gce.yml +++ b/gce.yml @@ -75,8 +75,8 @@ private: no - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates:\n" - default: "vpn" + prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + default: "vpnpw" private: yes roles: @@ -98,8 +98,8 @@ roles: - common - security - - vpn - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - vpn diff --git a/non-cloud.yml b/non-cloud.yml index a823cca1..b1f9f653 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -36,8 +36,8 @@ private: no - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates:\n" - default: "vpn" + prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + default: "vpnpw" private: yes - name: "IP_subject" @@ -76,8 +76,8 @@ roles: - common - security - - vpn - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - vpn diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 index 8c08f0f3..453a561a 100644 --- a/roles/common/templates/sshd_config.j2 +++ b/roles/common/templates/sshd_config.j2 @@ -54,7 +54,7 @@ MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@op # TODO: I haven't seen anyone review these yet # PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 -{% if ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "Y" %} +{% if ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" %} Match Group algo AllowTcpForwarding remote AllowAgentForwarding no diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index b78b19b3..63f6ceac 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -14,8 +14,22 @@ createhome: yes generate_ssh_key: yes shell: /bin/false - ssh_key_type: ecdsa - ssh_key_bits: 521 + ssh_key_type: rsa + ssh_key_bits: 2048 ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' + ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" state: present with_items: "{{ users }}" + +- name: The authorized keys file created + file: + src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' + dest: '/var/jail/{{ item }}/.ssh/authorized_keys' + owner: "{{ item }}" + group: algo + state: link + with_items: "{{ users }}" + +- name: Fetch users SSH private keys + fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + with_items: "{{ users }}" From 0945f54366708a3ca8725ec13b2eaa72762ad0ed Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 25 Aug 2016 23:30:27 +0300 Subject: [PATCH 093/769] SSH user-management #77 --- roles/ssh_tunneling/tasks/main.yml | 5 ++- users.yml | 62 ++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 63f6ceac..c3423647 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -9,7 +9,7 @@ - name: Ensure that the SSH users exist user: name: "{{ item }}" - group: algo + groups: algo home: '/var/jail/{{ item }}' createhome: yes generate_ssh_key: yes @@ -19,6 +19,7 @@ ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" state: present + append: yes with_items: "{{ users }}" - name: The authorized keys file created @@ -26,7 +27,7 @@ src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' dest: '/var/jail/{{ item }}/.ssh/authorized_keys' owner: "{{ item }}" - group: algo + group: "{{ item }}" state: link with_items: "{{ users }}" diff --git a/users.yml b/users.yml index 2e9e37e2..91544578 100644 --- a/users.yml +++ b/users.yml @@ -14,12 +14,17 @@ - name: "server_user" prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" default: "root" - private: no + private: no + + - name: "ssh_tunneling_enabled" + prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + default: "y" + private: no - name: "easyrsa_p12_export_password" - prompt: "Enter the password for p12 certificates:\n" - default: "vpn" - private: yes + prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + default: "vpnpw" + private: yes - name: "IP_subject" prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" @@ -33,6 +38,7 @@ ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" IP_subject: "{{ IP_subject }}" - name: Wait for SSH to become available @@ -114,3 +120,51 @@ - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes + + # SSH + + - name: SSH | Ensure that the system users exist + user: + name: "{{ item }}" + groups: algo + home: '/var/jail/{{ item }}' + createhome: yes + generate_ssh_key: yes + shell: /bin/false + ssh_key_type: rsa + ssh_key_bits: 2048 + ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' + ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" + state: present + append: yes + with_items: "{{ users }}" + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + + - name: SSH | The authorized keys file created + file: + src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' + dest: '/var/jail/{{ item }}/.ssh/authorized_keys' + owner: "{{ item }}" + group: "{{ item }}" + state: link + with_items: "{{ users }}" + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + + - name: SSH | Get active system users + shell: > + getent group algo | cut -f4 -d: | sed "s/,/\n/g" + register: valid_users + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + + - name: SSH | Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + with_items: "{{ valid_users.stdout_lines }}" + + - name: SSH | Fetch users SSH private keys + fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + with_items: "{{ users }}" From 57b6c96ba8ec2a1dca7d940dc2aaf60a61e745d9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 25 Aug 2016 23:48:35 +0300 Subject: [PATCH 094/769] SSH fingerprints #77 --- roles/ssh_tunneling/tasks/main.yml | 13 +++++++++++++ roles/ssh_tunneling/templates/known_hosts.j2 | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 roles/ssh_tunneling/templates/known_hosts.j2 diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index c3423647..2402f8f1 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -30,7 +30,20 @@ group: "{{ item }}" state: link with_items: "{{ users }}" + +- name: Generate SSH fingerprints + shell: > + ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null + register: ssh_fingerprints + +- name: The known_hosts file created + template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - name: Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" + +- name: Fetch the known_hosts file + fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}_known_hosts flat=yes + + diff --git a/roles/ssh_tunneling/templates/known_hosts.j2 b/roles/ssh_tunneling/templates/known_hosts.j2 new file mode 100644 index 00000000..98d33c4d --- /dev/null +++ b/roles/ssh_tunneling/templates/known_hosts.j2 @@ -0,0 +1,3 @@ +{% for item in ssh_fingerprints.stdout_lines %} +{{ item }} +{% endfor %} From 8c5f80bf8f467d4201304515fc245b7526af98c6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 25 Aug 2016 23:59:16 +0300 Subject: [PATCH 095/769] linting --- roles/ssh_tunneling/tasks/main.yml | 12 +++++------ users.yml | 32 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 2402f8f1..d3f2f5aa 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -21,29 +21,27 @@ state: present append: yes with_items: "{{ users }}" - + - name: The authorized keys file created - file: + file: src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' dest: '/var/jail/{{ item }}/.ssh/authorized_keys' owner: "{{ item }}" group: "{{ item }}" state: link with_items: "{{ users }}" - + - name: Generate SSH fingerprints shell: > ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null register: ssh_fingerprints - + - name: The known_hosts file created template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - name: Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" - + - name: Fetch the known_hosts file fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}_known_hosts flat=yes - - diff --git a/users.yml b/users.yml index 91544578..6401dd53 100644 --- a/users.yml +++ b/users.yml @@ -14,17 +14,17 @@ - name: "server_user" prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" default: "root" - private: no - + private: no + - name: "ssh_tunneling_enabled" prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" default: "y" - private: no - + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" default: "vpnpw" - private: yes + private: yes - name: "IP_subject" prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" @@ -51,7 +51,7 @@ become: true vars_files: - config.cfg - + pre_tasks: - set_fact: IP_subject_alt_name: "{{ IP_subject }}" @@ -66,7 +66,7 @@ creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' with_items: "{{ users }}" - - name: Build the client's p12 + - name: Build the client's p12 shell: > openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' @@ -84,7 +84,7 @@ - name: Revoke non-existing users shell: > - ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && + ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && ./easyrsa revoke {{ item }} && ipsec rereadcrls args: @@ -117,12 +117,12 @@ - name: Fetch users mobileconfig fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" - + - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes - + # SSH - + - name: SSH | Ensure that the system users exist user: name: "{{ item }}" @@ -138,10 +138,10 @@ state: present append: yes with_items: "{{ users }}" - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + - name: SSH | The authorized keys file created - file: + file: src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' dest: '/var/jail/{{ item }}/.ssh/authorized_keys' owner: "{{ item }}" @@ -160,11 +160,11 @@ user: name: "{{ item }}" state: absent - remove: yes + remove: yes force: yes when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" with_items: "{{ valid_users.stdout_lines }}" - + - name: SSH | Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" From 00e4bcc1ec6e72fe449e5632ea2cff4fdb30ee2d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 26 Aug 2016 00:35:07 +0300 Subject: [PATCH 096/769] security role and SSH fixes #77 --- digitalocean.yml | 7 ++++++- ec2.yml | 7 ++++++- gce.yml | 7 ++++++- non-cloud.yml | 8 +++++++- roles/cloud-digitalocean/tasks/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 1 + roles/cloud-gce/tasks/main.yml | 1 + roles/common/handlers/main.yml | 3 --- roles/common/tasks/main.yml | 5 ----- roles/security/handlers/main.yml | 3 +++ roles/security/tasks/main.yml | 5 +++++ .../{common => security}/templates/sshd_config.j2 | 0 roles/ssh_tunneling/tasks/main.yml | 14 ++++++++++++++ roles/vpn/tasks/main.yml | 14 ++++++++++++++ 14 files changed, 64 insertions(+), 12 deletions(-) rename roles/{common => security}/templates/sshd_config.j2 (100%) diff --git a/digitalocean.yml b/digitalocean.yml index 687bfbcc..fad6b34c 100644 --- a/digitalocean.yml +++ b/digitalocean.yml @@ -70,6 +70,11 @@ default: "y" private: no + - name: "security_enabled" + prompt: "Do you want to enable the security role? (y/n):\n" + default: "y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" default: "vpnpw" @@ -130,7 +135,7 @@ roles: - common - - security + - { role: security, when: security_enabled is defined and security_enabled == "y" } - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } diff --git a/ec2.yml b/ec2.yml index a988be6a..884e6433 100644 --- a/ec2.yml +++ b/ec2.yml @@ -76,6 +76,11 @@ default: "y" private: no + - name: "security_enabled" + prompt: "Do you want to enable the security role? (y/n):\n" + default: "y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" default: "vpnpw" @@ -99,7 +104,7 @@ roles: - common - - security + - { role: security, when: security_enabled is defined and security_enabled == "y" } - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } diff --git a/gce.yml b/gce.yml index 24a0cb95..599855ff 100644 --- a/gce.yml +++ b/gce.yml @@ -74,6 +74,11 @@ default: "y" private: no + - name: "security_enabled" + prompt: "Do you want to enable the security role? (y/n):\n" + default: "y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" default: "vpnpw" @@ -97,7 +102,7 @@ roles: - common - - security + - { role: security, when: security_enabled is defined and security_enabled == "y" } - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } diff --git a/non-cloud.yml b/non-cloud.yml index b1f9f653..8f5a33eb 100644 --- a/non-cloud.yml +++ b/non-cloud.yml @@ -35,6 +35,11 @@ default: "y" private: no + - name: "security_enabled" + prompt: "Do you want to enable the security role? (y/n):\n" + default: "y" + private: no + - name: "easyrsa_p12_export_password" prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" default: "vpnpw" @@ -54,6 +59,7 @@ dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" + security_enabled: "{{ security_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" IP_subject: "{{ IP_subject }}" @@ -75,7 +81,7 @@ roles: - common - - security + - { role: security, when: security_enabled is defined and security_enabled == "y" } - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 73e5c34d..ca8d7de9 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -34,6 +34,7 @@ dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" + security_enabled: "{{ security_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: digitalocean diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index cb211897..1bfb382a 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -72,6 +72,7 @@ dns_enabled: "{{ dns_enabled }}" proxy_enabled: "{{ proxy_enabled }}" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" + security_enabled: "{{ security_enabled }}" auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 661d9cba..f96690d9 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -24,6 +24,7 @@ proxy_enabled: "{{ proxy_enabled }}" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" auditd_enabled: " {{ auditd_enabled }}" + security_enabled: "{{ security_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: gce ipv6_support: no diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 6e249d75..c2296850 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -1,9 +1,6 @@ - name: restart rsyslog service: name=rsyslog state=restarted -- name: restart ssh - service: name=ssh state=restarted - - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index dc17b891..285fe6b4 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -30,11 +30,6 @@ when: reboot_required is defined and reboot_required.stdout == 'required' become: false -- name: SSH config - template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 - notify: - - restart ssh - - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" with_items: diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index ad1168b0..efb7ca4a 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,6 +1,9 @@ - name: restart rsyslog service: name=rsyslog state=restarted +- name: restart ssh + service: name=ssh state=restarted + - name: restart iptables service: name=netfilter-persistent state=restarted diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index a5288960..0f7ca097 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -100,3 +100,8 @@ - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: - restart iptables + +- name: SSH config + template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 + notify: + - restart ssh diff --git a/roles/common/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 similarity index 100% rename from roles/common/templates/sshd_config.j2 rename to roles/security/templates/sshd_config.j2 diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index d3f2f5aa..ea4d086e 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -1,5 +1,19 @@ --- +- name: Ensure that the sshd_config file has desired options + blockinfile: + dest: /etc/ssh/sshd_config + marker: '# ANSIBLE_MANAGED_ssh_tunneling_role' + block: | + Match Group algo + AllowTcpForwarding remote + AllowAgentForwarding no + AllowStreamLocalForwarding no + PermitTunnel no + X11Forwarding no + notify: + - restart ssh + - name: Ensure that the algo group exist group: name=algo state=present diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 1fe08b90..f658228a 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -20,6 +20,20 @@ - strongswan - netfilter-persistent +- name: Configure iptables so IPSec traffic can traverse the tunnel + iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE + when: (security_enabled is not defined) or + (security_enabled is defined and security_enabled != "y") + notify: + - save iptables + +- name: Configure ip6tables so IPSec traffic can traverse the tunnel + iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE + when: (security_enabled is not defined) or + (security_enabled is defined and security_enabled != "y") + notify: + - save iptables + - name: Ensure that the strongswan group exist group: name=strongswan state=present From 0cd4084aa45482598c8c5bb21e947ef9f18ad6aa Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 26 Aug 2016 00:47:08 +0300 Subject: [PATCH 097/769] ssh fixes --- roles/security/templates/sshd_config.j2 | 8 -------- 1 file changed, 8 deletions(-) diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index 453a561a..c014eb46 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -54,11 +54,3 @@ MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@op # TODO: I haven't seen anyone review these yet # PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 -{% if ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" %} -Match Group algo - AllowTcpForwarding remote - AllowAgentForwarding no - AllowStreamLocalForwarding no - PermitTunnel no - X11Forwarding no -{% endif %} From 0a54e26cc79cf92f4b381198667a47817e6f3b59 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 28 Aug 2016 00:48:23 +0200 Subject: [PATCH 098/769] Better description of roles --- README.md | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5c878a0..07bb4339 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Algo +# Algo VPN [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) -Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco)) is a set of Ansible scripts that simplifies the setup of an IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. +Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco)) is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. ## Features @@ -23,6 +23,41 @@ Algo (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere * Does not claim to provide anonymity or censorship avoidance * Does not claim to protect you from the [FSB](https://en.wikipedia.org/wiki/Federal_Security_Service), [MSS](https://en.wikipedia.org/wiki/Ministry_of_State_Security_(China)), [DGSE](https://en.wikipedia.org/wiki/Directorate-General_for_External_Security), or [FSM](https://en.wikipedia.org/wiki/Flying_Spaghetti_Monster) +## Included Roles + +Ansible scripts are organized into roles, each of which provides one discrete set of functionality. The roles used by Algo are described in detail below. + +### Required Roles + +* **Common** + * Installs several required packages and software updates, then reboots if necessary + * Configures network interfaces and enables packet forwarding on them +* **VPN** + * Installs [StrongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges + * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user + * Bundles the appropriate certificates into Apple mobileconfig profiles for each user + +### Optional Roles + +* **Security Enhancements** + * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure your server is always patched to avoid the latest vulnerabilities. + * Minimizes the exposure of SUID binaries, restricts core dumps, and modifies kernel features to limit possible attacks. + * Modifies SSH to only use modern ciphers and a seccomp sandbox, and restricts access to many legacy and unwanted features, like X11 forwarding and SFTP. + * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834). +* **Ad Blocking and Compression HTTP Proxy** + * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset. + * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy. + * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations. +* **DNS Ad Blocking** + * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains. + * Constraints dnsmasq with AppArmor and cgroups CPU and memory limitations. +* **Security Monitoring and Logging** + * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents. + * Logs are aggregated and emailed to the address in `config.cfg` on a regular basis. +* **SSH Tunneling** + * Adds a restricted `algo` group to `sshd_config` with no shell access and limited forwarding options. + * Creates one local account per user and creates an SSH public key for each. + ## Usage ### Requirements @@ -75,7 +110,6 @@ If you want to add or delete users, update the `users` list in `config.cfg` and ./algo update-users ``` - ## FAQ ### Has this been audited? From 7c418be9a823a66d3d90331ad2884205103ef13d Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 28 Aug 2016 01:00:10 +0200 Subject: [PATCH 099/769] Update README.md --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 07bb4339..d72329fe 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw ## Included Roles -Ansible scripts are organized into roles, each of which provides one discrete set of functionality. The roles used by Algo are described in detail below. +Ansible scripts are organized into roles. The roles used by Algo are described in detail below. ### Required Roles @@ -40,23 +40,23 @@ Ansible scripts are organized into roles, each of which provides one discrete se ### Optional Roles * **Security Enhancements** - * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure your server is always patched to avoid the latest vulnerabilities. - * Minimizes the exposure of SUID binaries, restricts core dumps, and modifies kernel features to limit possible attacks. - * Modifies SSH to only use modern ciphers and a seccomp sandbox, and restricts access to many legacy and unwanted features, like X11 forwarding and SFTP. - * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834). + * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied + * Modify operating system features like core dumps, kernel parameters, and SUID binaries to limit possible attacks + * Modifies SSH to use only modern ciphers and a seccomp sandbox, and restricts access to many legacy and unwanted features, like X11 forwarding and SFTP + * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) * **Ad Blocking and Compression HTTP Proxy** - * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset. - * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy. - * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations. + * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset + * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy + * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations * **DNS Ad Blocking** - * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains. - * Constraints dnsmasq with AppArmor and cgroups CPU and memory limitations. + * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains + * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations * **Security Monitoring and Logging** - * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents. - * Logs are aggregated and emailed to the address in `config.cfg` on a regular basis. + * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents + * Emails aggregated Logs to a configured address on a regular basis * **SSH Tunneling** - * Adds a restricted `algo` group to `sshd_config` with no shell access and limited forwarding options. - * Creates one local account per user and creates an SSH public key for each. + * Adds a restricted `algo` group to SSH with no shell access and limited forwarding options + * Creates one limited, local account per user and an SSH public key for each ## Usage From 4284dd63aae28207bcb89aaf8a46bc0a11f83901 Mon Sep 17 00:00:00 2001 From: Evgeniy Ivanov Date: Sun, 28 Aug 2016 22:06:33 +0300 Subject: [PATCH 100/769] rsyslog moved to the logging role --- roles/logging/handlers/main.yml | 3 +++ roles/logging/tasks/main.yml | 17 +++++++++++++++++ .../{security => logging}/templates/CIS.conf.j2 | 0 .../templates/rsyslog.conf.j2 | 0 roles/security/handlers/main.yml | 3 --- roles/security/tasks/main.yml | 15 --------------- 6 files changed, 20 insertions(+), 18 deletions(-) rename roles/{security => logging}/templates/CIS.conf.j2 (100%) rename roles/{security => logging}/templates/rsyslog.conf.j2 (100%) diff --git a/roles/logging/handlers/main.yml b/roles/logging/handlers/main.yml index 651d8a7d..9dcd122d 100644 --- a/roles/logging/handlers/main.yml +++ b/roles/logging/handlers/main.yml @@ -1,2 +1,5 @@ +- name: restart rsyslog + service: name=rsyslog state=restarted + - name: restart auditd service: name=auditd state=restarted diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml index fdda9376..48ed4796 100644 --- a/roles/logging/tasks/main.yml +++ b/roles/logging/tasks/main.yml @@ -1,3 +1,5 @@ +# Auditd + - name: Auditd installed apt: name=auditd state=latest @@ -13,3 +15,18 @@ - name: Enable services service: name=auditd enabled=yes + +# Rsyslog + +- name: Rsyslog configured + template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf + notify: + - restart rsyslog + +- name: Rsyslog CIS configured + template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 + notify: + - restart rsyslog + +- name: Enable services + service: name=rsyslog enabled=yes diff --git a/roles/security/templates/CIS.conf.j2 b/roles/logging/templates/CIS.conf.j2 similarity index 100% rename from roles/security/templates/CIS.conf.j2 rename to roles/logging/templates/CIS.conf.j2 diff --git a/roles/security/templates/rsyslog.conf.j2 b/roles/logging/templates/rsyslog.conf.j2 similarity index 100% rename from roles/security/templates/rsyslog.conf.j2 rename to roles/logging/templates/rsyslog.conf.j2 diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index efb7ca4a..e79c49c0 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,6 +1,3 @@ -- name: restart rsyslog - service: name=rsyslog state=restarted - - name: restart ssh service: name=ssh state=restarted diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 0f7ca097..c46e041d 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -30,21 +30,6 @@ executable: /bin/bash register: privileged_programs -# Rsyslog - -- name: Rsyslog configured - template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf - notify: - - restart rsyslog - -- name: Rsyslog CIS configured - template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 - notify: - - restart rsyslog - -- name: Enable services - service: name=rsyslog enabled=yes - # Core dumps - name: Restrict core dumps (with PAM) From 05df4f0c0403ea4486d627e6e80622b0e7ccb82d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 28 Aug 2016 22:11:39 +0300 Subject: [PATCH 101/769] unattended-upgrades moved to the security role --- roles/common/tasks/main.yml | 7 ------- roles/security/tasks/main.yml | 11 +++++++++++ roles/{common => security}/templates/10periodic.j2 | 0 .../templates/50unattended-upgrades.j2 | 0 4 files changed, 11 insertions(+), 7 deletions(-) rename roles/{common => security}/templates/10periodic.j2 (100%) rename roles/{common => security}/templates/50unattended-upgrades.j2 (100%) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 285fe6b4..922bc75f 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -46,16 +46,9 @@ - coreutils - rsyslog - sendmail - - unattended-upgrades - iptables-persistent - cgroup-tools -- name: Configure unattended-upgrades - template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 - -- name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 - - name: Loopback for services configured template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg notify: diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index c46e041d..7046e2c5 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,3 +1,14 @@ +- name: Install tools + apt: name="{{ item }}" state=latest + with_items: + - unattended-upgrades + +- name: Configure unattended-upgrades + template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 + +- name: Periodic upgrades configured + template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 + # Using a two-pass approach for checking directories in order to support symlinks. - name: Find directories for minimizing access stat: diff --git a/roles/common/templates/10periodic.j2 b/roles/security/templates/10periodic.j2 similarity index 100% rename from roles/common/templates/10periodic.j2 rename to roles/security/templates/10periodic.j2 diff --git a/roles/common/templates/50unattended-upgrades.j2 b/roles/security/templates/50unattended-upgrades.j2 similarity index 100% rename from roles/common/templates/50unattended-upgrades.j2 rename to roles/security/templates/50unattended-upgrades.j2 From 97a00699b7d8b47c783bc86d177ec22cec711e41 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 28 Aug 2016 23:04:59 +0300 Subject: [PATCH 102/769] new tags --- config.cfg | 2 + deploy.yml | 39 +++++++ digitalocean.yml | 147 ------------------------ playbooks/common.yml | 5 + playbooks/digitalocean.yml | 114 ++++++++++++++++++ roles/cloud-digitalocean/tasks/main.yml | 7 +- roles/security/tasks/main.yml | 1 - 7 files changed, 161 insertions(+), 154 deletions(-) create mode 100644 deploy.yml delete mode 100644 digitalocean.yml create mode 100644 playbooks/common.yml create mode 100644 playbooks/digitalocean.yml diff --git a/config.cfg b/config.cfg index c8178b58..cd827f35 100644 --- a/config.cfg +++ b/config.cfg @@ -13,6 +13,8 @@ auditd_action_mail_acct: email@example.com easyrsa_dir: /opt/easy-rsa-ipsec easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 +easyrsa_p12_export_password: vpnpws + # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False diff --git a/deploy.yml b/deploy.yml new file mode 100644 index 00000000..d69ed68b --- /dev/null +++ b/deploy.yml @@ -0,0 +1,39 @@ +- name: Configure the server and install required software + hosts: localhost + vars_files: + - config.cfg + + roles: + - { role: cloud-digitalocean, tags: ['digitalocean'] } + - { role: cloud-ec2, tags: ['ec2'] } + - { role: cloud-gce, tags: ['gce'] } + +- name: Post-provisioning tasks + hosts: vpn-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Common pre-tasks + include: playbooks/common.yml + tags: [ 'digitalocean', 'ec2', 'gce' ] + + - name: DigitalOcean pre-tasks + include: playbooks/digitalocean.yml + tags: [ 'digitalocean', 'ec2', 'gce' ] + + roles: + - { role: common, tags: [ 'vpn' ] } + - { role: security, tags: [ 'security' ] } + - { role: proxy, tags: [ 'proxy', 'adblock' ] } + - { role: dns_adblocking, tags: ['dns', 'adblock' ] } + - { role: logging, tags: [ 'logging' ] } + - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } + - { role: vpn, tags: [ 'vpn' ] } + + + handlers: + - name: reload eth0 + shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' \ No newline at end of file diff --git a/digitalocean.yml b/digitalocean.yml deleted file mode 100644 index fad6b34c..00000000 --- a/digitalocean.yml +++ /dev/null @@ -1,147 +0,0 @@ -# vim:ft=ansible: -- name: Configure the server and install required software - hosts: localhost - - vars: - regions: - "1": "ams2" - "2": "ams3" - "3": "fra1" - "4": "lon1" - "5": "nyc1" - "6": "nyc2" - "7": "nyc3" - "8": "sfo1" - "9": "sfo2" - "10": "sgp1" - "11": "tor1" - "12": "blr1" - - vars_prompt: - - name: "do_access_token" - prompt: "Enter your API Token (https://cloud.digitalocean.com/settings/api/tokens):\n" - private: yes - - - name: "do_ssh_name" - prompt: "Enter a valid SSH key name (https://cloud.digitalocean.com/settings/security):\n" - private: no - - - name: "do_region" - prompt: > - What region should the server be located in? - 1. Amsterdam (Datacenter 2) - 2. Amsterdam (Datacenter 3) - 3. Frankfurt - 4. London - 5. New York (Datacenter 1) - 6. New York (Datacenter 2) - 7. New York (Datacenter 3) - 8. San Francisco (Datacenter 1) - 9. San Francisco (Datacenter 2) - 10. Singapore - 11. Toronto - 12. Bangalore - Enter the number of your desired region: - default: "7" - private: no - - - name: "do_server_name" - prompt: "Name the vpn server:\n" - default: "algo.local" - private: no - - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" - default: "y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" - default: "y" - private: no - - - name: "auditd_enabled" - prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" - default: "y" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "y" - private: no - - - name: "security_enabled" - prompt: "Do you want to enable the security role? (y/n):\n" - default: "y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - default: "vpnpw" - private: yes - - roles: - - cloud-digitalocean - -- name: Post-provisioning tasks - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - - name: Enable IPv6 on the droplet - uri: - url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" - method: POST - body: - type: enable_ipv6 - body_format: json - status_code: 201 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - - - name: Get Droplet networks - uri: - url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" - method: GET - status_code: 200 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - register: droplet_info - - - name: IPv6 configured - template: src=roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 - with_items: "{{ droplet_info.json.droplet.networks.v6 }}" - notify: - - reload eth0 - - - name: IPv6 included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/20-ipv6.cfg' state=present - notify: - - reload eth0 - - - meta: flush_handlers - - - name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" - become: false - - roles: - - common - - { role: security, when: security_enabled is defined and security_enabled == "y" } - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - - { role: dns_adblocking, when: dns_enabled is defined and dns_enabled == "y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - vpn - - handlers: - - name: reload eth0 - shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' diff --git a/playbooks/common.yml b/playbooks/common.yml new file mode 100644 index 00000000..1cf52830 --- /dev/null +++ b/playbooks/common.yml @@ -0,0 +1,5 @@ +- name: Install prerequisites + raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + +- name: Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 diff --git a/playbooks/digitalocean.yml b/playbooks/digitalocean.yml new file mode 100644 index 00000000..89734472 --- /dev/null +++ b/playbooks/digitalocean.yml @@ -0,0 +1,114 @@ + #vars: + #regions: + #"1": "ams2" + #"2": "ams3" + #"3": "fra1" + #"4": "lon1" + #"5": "nyc1" + #"6": "nyc2" + #"7": "nyc3" + #"8": "sfo1" + #"9": "sfo2" + #"10": "sgp1" + #"11": "tor1" + #"12": "blr1" + + #vars_prompt: + #- name: "do_access_token" + #prompt: "Enter your API Token (https://cloud.digitalocean.com/settings/api/tokens):\n" + #private: yes + + #- name: "do_ssh_name" + #prompt: "Enter a valid SSH key name (https://cloud.digitalocean.com/settings/security):\n" + #private: no + + #- name: "do_region" + #prompt: > + #What region should the server be located in? + #1. Amsterdam (Datacenter 2) + #2. Amsterdam (Datacenter 3) + #3. Frankfurt + #4. London + #5. New York (Datacenter 1) + #6. New York (Datacenter 2) + #7. New York (Datacenter 3) + #8. San Francisco (Datacenter 1) + #9. San Francisco (Datacenter 2) + #10. Singapore + #11. Toronto + #12. Bangalore + #Enter the number of your desired region: + #default: "7" + #private: no + + #- name: "do_server_name" + #prompt: "Name the vpn server:\n" + #default: "algo.local" + #private: no + + #- name: "dns_enabled" + #prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" + #default: "y" + #private: no + + #- name: "proxy_enabled" + #prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" + #default: "y" + #private: no + + #- name: "auditd_enabled" + #prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" + #default: "y" + #private: no + + #- name: "ssh_tunneling_enabled" + #prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" + #default: "y" + #private: no + + #- name: "security_enabled" + #prompt: "Do you want to enable the security role? (y/n):\n" + #default: "y" + #private: no + + #- name: "easyrsa_p12_export_password" + #prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" + #default: "vpnpw" + #private: yes + +- name: Enable IPv6 on the droplet + uri: + url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" + method: POST + body: + type: enable_ipv6 + body_format: json + status_code: 201 + HEADER_Authorization: "Bearer {{ do_access_token }}" + HEADER_Content-Type: "application/json" + +- name: Get Droplet networks + uri: + url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" + method: GET + status_code: 200 + HEADER_Authorization: "Bearer {{ do_access_token }}" + HEADER_Content-Type: "application/json" + register: droplet_info + +- name: IPv6 configured + template: src=roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 + with_items: "{{ droplet_info.json.droplet.networks.v6 }}" + notify: + - reload eth0 + +- name: IPv6 included into the network config + lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/20-ipv6.cfg' state=present + notify: + - reload eth0 + +- meta: flush_handlers + +- name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" + become: false \ No newline at end of file diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index ca8d7de9..06bfba17 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -15,7 +15,7 @@ state: present command: droplet name: "{{ do_server_name }}" - region_id: "{{ regions[do_region] }}" + region_id: "{{ do_region }}" size_id: "512mb" image_id: "ubuntu-16-04-x64" ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" @@ -31,11 +31,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" - dns_enabled: "{{ dns_enabled }}" - proxy_enabled: "{{ proxy_enabled }}" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - security_enabled: "{{ security_enabled }}" - auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: digitalocean ipv6_support: yes diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 7046e2c5..6ad36c56 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -9,7 +9,6 @@ - name: Periodic upgrades configured template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 -# Using a two-pass approach for checking directories in order to support symlinks. - name: Find directories for minimizing access stat: path: "{{ item }}" From ddcee8db18517eb5af0291a8e795083ddb84f958 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 28 Aug 2016 23:07:45 +0300 Subject: [PATCH 103/769] logging fixes --- roles/common/tasks/main.yml | 1 - roles/logging/tasks/main.yml | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 922bc75f..44aa3452 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -44,7 +44,6 @@ - apparmor-utils - uuid-runtime - coreutils - - rsyslog - sendmail - iptables-persistent - cgroup-tools diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml index 48ed4796..821157ec 100644 --- a/roles/logging/tasks/main.yml +++ b/roles/logging/tasks/main.yml @@ -18,6 +18,9 @@ # Rsyslog +- name: Rsyslog installed + apt: name=rsyslog state=latest + - name: Rsyslog configured template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf notify: From 91688324ce8804eb253bc47b3e27e8e8f1ae5ee1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 28 Aug 2016 23:19:41 +0300 Subject: [PATCH 104/769] additional functions --- deploy.yml | 1 - roles/cloud-ec2/tasks/main.yml | 5 ----- roles/cloud-gce/tasks/main.yml | 5 ----- roles/dns_adblocking/meta/main.yml | 4 ++++ roles/logging/meta/main.yml | 4 ++++ roles/proxy/meta/main.yml | 4 ++++ roles/security/meta/main.yml | 4 ++++ roles/ssh_tunneling/meta/main.yml | 4 ++++ roles/vpn/handlers/main.yml | 3 +++ roles/vpn/meta/main.yml | 4 ++++ 10 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 roles/dns_adblocking/meta/main.yml create mode 100644 roles/logging/meta/main.yml create mode 100644 roles/proxy/meta/main.yml create mode 100644 roles/security/meta/main.yml create mode 100644 roles/ssh_tunneling/meta/main.yml create mode 100644 roles/vpn/meta/main.yml diff --git a/deploy.yml b/deploy.yml index d69ed68b..e6bcc9d7 100644 --- a/deploy.yml +++ b/deploy.yml @@ -25,7 +25,6 @@ tags: [ 'digitalocean', 'ec2', 'gce' ] roles: - - { role: common, tags: [ 'vpn' ] } - { role: security, tags: [ 'security' ] } - { role: proxy, tags: [ 'proxy', 'adblock' ] } - { role: dns_adblocking, tags: ['dns', 'adblock' ] } diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 1bfb382a..6e1a9b0e 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -69,11 +69,6 @@ groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" - dns_enabled: "{{ dns_enabled }}" - proxy_enabled: "{{ proxy_enabled }}" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - security_enabled: "{{ security_enabled }}" - auditd_enabled: " {{ auditd_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 ipv6_support: no diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index f96690d9..07ce08ba 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -20,11 +20,6 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" - dns_enabled: "{{ dns_enabled }}" - proxy_enabled: "{{ proxy_enabled }}" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - auditd_enabled: " {{ auditd_enabled }}" - security_enabled: "{{ security_enabled }}" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: gce ipv6_support: no diff --git a/roles/dns_adblocking/meta/main.yml b/roles/dns_adblocking/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/dns_adblocking/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file diff --git a/roles/logging/meta/main.yml b/roles/logging/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/logging/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file diff --git a/roles/proxy/meta/main.yml b/roles/proxy/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/proxy/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file diff --git a/roles/security/meta/main.yml b/roles/security/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/security/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file diff --git a/roles/ssh_tunneling/meta/main.yml b/roles/ssh_tunneling/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/ssh_tunneling/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index a3c10f7d..a8e921a4 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -6,6 +6,9 @@ - name: restart apparmor service: name=apparmor state=restarted + +- name: save iptables + shell: service netfilter-persistent save - name: congrats debug: diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml new file mode 100644 index 00000000..325ccd93 --- /dev/null +++ b/roles/vpn/meta/main.yml @@ -0,0 +1,4 @@ +--- + +dependencies: + - { role: common } \ No newline at end of file From 9804df37281b948b1743f41a024a426b97bb821e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 29 Aug 2016 00:05:20 +0300 Subject: [PATCH 105/769] global tags --- deploy.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy.yml b/deploy.yml index e6bcc9d7..dd5b131a 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,5 +1,6 @@ - name: Configure the server and install required software hosts: localhost + tags: algo vars_files: - config.cfg @@ -11,6 +12,7 @@ - name: Post-provisioning tasks hosts: vpn-host gather_facts: false + tags: algo become: true vars_files: - config.cfg @@ -35,4 +37,5 @@ handlers: - name: reload eth0 - shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' \ No newline at end of file + shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' + \ No newline at end of file From 6685642f0ba3208d7bf70d6e968e72402a105b72 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 31 Aug 2016 11:42:29 +0300 Subject: [PATCH 106/769] #85 fixed --- roles/vpn/handlers/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index a3c10f7d..a8e921a4 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -6,6 +6,9 @@ - name: restart apparmor service: name=apparmor state=restarted + +- name: save iptables + shell: service netfilter-persistent save - name: congrats debug: From 4fc0528a2580887f11dc1dbd45f39d2ae33d5f35 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 6 Sep 2016 23:10:08 +0200 Subject: [PATCH 107/769] more references for OpenVPN --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d72329fe..b1621456 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ### Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to update and maintain the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due to [past security incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due [to](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) [past](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [security](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/) [incidents](https://www.exploit-db.com/exploits/34879/). ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? From 3522ea5cc8f458569d335a6847fa202317457efe Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 6 Sep 2016 23:11:50 +0200 Subject: [PATCH 108/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1621456..7896c9a2 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ### Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due [to](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) [past](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [security](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/) [incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to past [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? From 1feb1dd93746c54d5b312576f5b812bb81917b43 Mon Sep 17 00:00:00 2001 From: jack Date: Sun, 18 Sep 2016 13:10:38 +0300 Subject: [PATCH 109/769] remove unused files --- README.md | 2 +- ec2.yml | 112 ------------------------------------------------------ gce.yml | 110 ----------------------------------------------------- 3 files changed, 1 insertion(+), 223 deletions(-) delete mode 100644 ec2.yml delete mode 100644 gce.yml diff --git a/README.md b/README.md index d72329fe..7896c9a2 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ### Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to update and maintain the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the protocol and its implementations, and we simply trust the server less due to [past security incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to past [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? diff --git a/ec2.yml b/ec2.yml deleted file mode 100644 index 884e6433..00000000 --- a/ec2.yml +++ /dev/null @@ -1,112 +0,0 @@ -# vim:ft=ansible: -- name: Create a sandbox instance - hosts: localhost - gather_facts: False - vars_files: - - config.cfg - vars: - instance_type: t2.nano - security_group: vpn-secgroup - regions: - "1": "us-east-1" - "2": "us-west-1" - "3": "us-west-2" - "4": "ap-south-1" - "5": "ap-northeast-2" - "6": "ap-southeast-1" - "7": "ap-southeast-2" - "8": "ap-northeast-1" - "9": "eu-central-1" - "10": "eu-west-1" - "11": "sa-east-1" - - vars_prompt: - - name: "aws_access_key" - prompt: "Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" - private: yes - - - name: "aws_secret_key" - prompt: "Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html):\n" - private: yes - - - name: "region" - prompt: > - What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-west-1 US West (N. California) - 3. us-west-2 US West (Oregon) - 4. ap-south-1 Asia Pacific (Mumbai) - 5. ap-northeast-2 Asia Pacific (Seoul) - 6. ap-southeast-1 Asia Pacific (Singapore) - 7. ap-southeast-2 Asia Pacific (Sydney) - 8. ap-northeast-1 Asia Pacific (Tokyo) - 9. eu-central-1 EU (Frankfurt) - 10. eu-west-1 EU (Ireland) - 11. sa-east-1 South America (São Paulo) - default: "1" - private: no - - - name: "aws_server_name" - prompt: "Name the vpn server:\n" - default: "algo.local" - private: no - - - name: "ssh_public_key" - prompt: "Enter the local path to your SSH public key:\n" - default: "~/.ssh/id_rsa.pub" - private: no - - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" - default: "y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" - default: "y" - private: no - - - name: "auditd_enabled" - prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" - default: "y" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "y" - private: no - - - name: "security_enabled" - prompt: "Do you want to enable the security role? (y/n):\n" - default: "y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - default: "vpnpw" - private: yes - - roles: - - cloud-ec2 - -- name: Post-provisioning tasks - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - roles: - - common - - { role: security, when: security_enabled is defined and security_enabled == "y" } - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - vpn diff --git a/gce.yml b/gce.yml deleted file mode 100644 index 599855ff..00000000 --- a/gce.yml +++ /dev/null @@ -1,110 +0,0 @@ -# vim:ft=ansible: -- name: Configure the server and install required software - hosts: localhost - gather_facts: false - - vars: - zones: - "1": "us-central1-a" - "2": "us-central1-b" - "3": "us-central1-c" - "4": "us-central1-f" - "5": "us-east1-b" - "6": "us-east1-c" - "7": "us-east1-d" - "8": "europe-west1-b" - "9": "europe-west1-c" - "10": "europe-west1-d" - "11": "asia-east1-a" - "12": "asia-east1-b" - "13": "asia-east1-c" - - vars_prompt: - - name: "credentials_file" - prompt: "Enter the local path to your credentials JSON file [ex: ~/gogle_cloud.json] (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts):\n" - private: no - - - name: "ssh_public_key" - prompt: "Enter the local path to your SSH public key:\n" - default: "~/.ssh/id_rsa.pub" - private: no - - - name: "zone" - prompt: > - What zone should the server be located in? - 1. Central US (Iowa A) - 2. Central US (Iowa B) - 3. Central US (Iowa C) - 4. Central US (Iowa F) - 5. Eastern US (South Carolina B) - 6. Eastern US (South Carolina C) - 7. Eastern US (South Carolina D) - 8. Western Europe (Belgium B) - 9. Western Europe (Belgium C) - 10. Western Europe (Belgium D) - 11. East Asia (Taiwan A) - 12. East Asia (Taiwan B) - 13. East Asia (Taiwan C) - Please choose the number of your zone. Press enter for default (#8) zone. - default: "8" - private: no - - - name: "server_name" - prompt: "Name the vpn server:\n" - default: "algo" - private: no - - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" - default: "y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" - default: "y" - private: no - - - name: "auditd_enabled" - prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" - default: "y" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "y" - private: no - - - name: "security_enabled" - prompt: "Do you want to enable the security role? (y/n):\n" - default: "y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - default: "vpnpw" - private: yes - - roles: - - cloud-gce - -- name: Post-provisioning tasks - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - roles: - - common - - { role: security, when: security_enabled is defined and security_enabled == "y" } - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - vpn From 97ea00056df01d5882dd2b8b4398d731ccab9f6a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:11:10 +0300 Subject: [PATCH 110/769] DO roles to tags --- roles/cloud-digitalocean/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 06bfba17..5a28f8f7 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,6 +1,6 @@ - name: Set the DigitalOcean Access Token fact set_fact: - do_token: "{{ do_access_token | default( lookup('env', 'DIGITALOCEAN_API_KEY') ) }}" + do_token: "{{ do_access_token }}" - name: "Getting your SSH key ID on Digital Ocean..." digital_ocean: From cf5a0f41d3aa05320991e6773fe2834e1195b3ab Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:11:22 +0300 Subject: [PATCH 111/769] ec2 role to tags --- roles/cloud-ec2/tasks/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 6e1a9b0e..4f25e1b0 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -7,7 +7,7 @@ sort: name sort_order: descending sort_end: 1 - region: "{{ regions[region] }}" + region: "{{ region }}" register: ami_search - set_fact: @@ -18,7 +18,7 @@ aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" name: VPNKEY - region: "{{ regions[region] }}" + region: "{{ region }}" key_material: "{{ item }}" with_file: "{{ ssh_public_key }}" register: keypair @@ -27,9 +27,9 @@ ec2_group: aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" - name: "{{ security_group }}" + name: vpn-secgroup description: Security group for VPN servers - region: "{{ regions[region] }}" + region: "{{ region }}" rules: - proto: udp from_port: 4500 @@ -54,11 +54,11 @@ aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" keypair: "VPNKEY" - group: "{{ security_group }}" - instance_type: "{{ instance_type }}" + group: vpn-secgroup + instance_type: t2.nano image: "{{ ami_image }}" wait: true - region: "{{ regions[region] }}" + region: "{{ region }}" instance_tags: name: "{{ aws_server_name }}" register: ec2 From aa4dcc31d454dbcdc64a46867f1bb3776ae5fb96 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:11:30 +0300 Subject: [PATCH 112/769] gce role to tags --- roles/cloud-gce/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 07ce08ba..9c12f479 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -5,7 +5,7 @@ - name: "Creating a new instance..." gce: instance_names: "{{ server_name }}" - zone: "{{ zones[zone] }}" + zone: "{{ zone }}" machine_type: n1-standard-1 image: ubuntu-1604 service_account_email: "{{ credentials_file_lookup.client_email }}" From d9441b236a70036881d17f5ed484683ffbb030e1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:12:17 +0300 Subject: [PATCH 113/769] move to tags #80 --- deploy.yml | 9 ++++----- roles/vpn/meta/main.yml | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy.yml b/deploy.yml index dd5b131a..bca20ee3 100644 --- a/deploy.yml +++ b/deploy.yml @@ -20,11 +20,11 @@ pre_tasks: - name: Common pre-tasks include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce' ] + tags: [ 'digitalocean', 'ec2', 'gce', 'pre' ] - name: DigitalOcean pre-tasks include: playbooks/digitalocean.yml - tags: [ 'digitalocean', 'ec2', 'gce' ] + tags: [ 'digitalocean' ] roles: - { role: security, tags: [ 'security' ] } @@ -32,10 +32,9 @@ - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - { role: logging, tags: [ 'logging' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - - { role: vpn, tags: [ 'vpn' ] } - + - { role: vpn, tags: [ 'vpn' ] } handlers: - name: reload eth0 shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' - \ No newline at end of file + diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index 325ccd93..149a6fbf 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -1,4 +1,5 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common } + From b41a8d58cdfb142d022097bb5cb9ec8e806eafb2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:14:32 +0300 Subject: [PATCH 114/769] extend README to use roles --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7896c9a2..22924520 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,86 @@ Ansible scripts are organized into roles. The roles used by Algo are described i * SHell or BASH * libselinux-python (for RedHat based distros) +### Roles and Tags +**Cloud roles:** +- role: cloud-digitalocean, tags: digitalocean +- role: cloud-ec2, tags: ec2 +- role: cloud-gce, tags: gce + +**Server roles:** +- role: vpn, tags: vpn +- role: dns_adblocking, tags: dns, adblock +- role: proxy, tags: proxy, adblock +- role: logging, tags: logging +- role: security, tags: security +- role: ssh_tunneling, tags: ssh_tunneling + +### Cloud Providers + +**digitalocean** +*Requirement variables:* +- do_access_token +- do_ssh_name +- do_server_name +- do_region + +*Possible regions:* +- ams2 +- ams3 +- fra1 +- lon1 +- nyc1 +- nyc2 +- nyc3 +- sfo1 +- sfo2 +- sgp1 +- tor1 +- blr1 + +**gce** +*Requirement variables:* +- credentials_file +- server_name +- ssh_public_key +- zone + +*Possible zones:* +- us-central1-a +- us-central1-b +- us-central1-c +- us-central1-f +- us-east1-b +- us-east1-c +- us-east1-d +- europe-west1-b +- europe-west1-c +- europe-west1-d +- asia-east1-a +- asia-east1-b +- asia-east1-c + +**ec2** +*Requirement variables:* +- aws_access_key +- aws_secret_key +- aws_server_name +- ssh_public_key +- region + +*Possible regions:* +- us-east-1 +- us-west-1 +- us-west-2 +- ap-south-1 +- ap-northeast-2 +- ap-southeast-1 +- ap-southeast-2 +- ap-northeast-1 +- eu-central-1 +- eu-west-1 +- sa-east-1 + ### Cloud Deployment To install the dependencies on OS X or Linux: @@ -84,10 +164,11 @@ sudo pip install -r requirements.txt Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -Start the deploy and follow the instructions: +Start the deploy with extra variables and tags that you need. +Example for DigitalOcean: ``` -./algo +ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=secret_token do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' ``` When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. From 4efca40675cd4a8f3ae41e6bc51daa1b25109835 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:40:46 +0300 Subject: [PATCH 115/769] DO prompts --- algo | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 8283b3c3..5ad4e95f 100755 --- a/algo +++ b/algo @@ -2,6 +2,57 @@ set -e +digitalocean () { + read -p " +Enter your API Token (https://cloud.digitalocean.com/settings/api/tokens): +: " -rs do_access_token + + read -p " +Enter a valid SSH key name (https://cloud.digitalocean.com/settings/security): +: " -r do_ssh_name + + read -p " +Name the vpn server: +[algo.local]: " -r do_server_name + do_server_name=${do_server_name:-algo.local} + + read -p " + What region should the server be located in? + 1. Amsterdam (Datacenter 2) + 2. Amsterdam (Datacenter 3) + 3. Frankfurt + 4. London + 5. New York (Datacenter 1) + 6. New York (Datacenter 2) + 7. New York (Datacenter 3) + 8. San Francisco (Datacenter 1) + 9. San Francisco (Datacenter 2) + 10. Singapore + 11. Toronto + 12. Bangalore +Enter the number of your desired region: +[7]: " -r region + region=${region:-1} + + case "$region" in + 1) do_region="ams2" ;; + 2) do_region="ams3" ;; + 3) do_region="fra1" ;; + 4) do_region="lon1" ;; + 5) do_region="nyc1" ;; + 6) do_region="nyc2" ;; + 7) do_region="nyc3" ;; + 8) do_region="sfo1" ;; + 9) do_region="sfo2" ;; + 10) do_region="sgp1" ;; + 11) do_region="tor1" ;; + 12) do_region="blr1" ;; + esac + +ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" + +} + algo_provisioning () { echo -n " What provider would you like to use? @@ -16,7 +67,7 @@ Enter the number of your desired provider read -r N case "$N" in - 1) CLOUD="digitalocean" ;; + 1) digitalocean; ;; 2) CLOUD="ec2" ;; 3) CLOUD="gce" ;; 4) CLOUD="non-cloud" ;; From 6bc9e9a1801056063829741ad1f7b9a8c9444bc4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 13:52:49 +0300 Subject: [PATCH 116/769] EC2 prompts --- algo | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/algo b/algo index 5ad4e95f..fd5a012a 100755 --- a/algo +++ b/algo @@ -32,7 +32,7 @@ Name the vpn server: 12. Bangalore Enter the number of your desired region: [7]: " -r region - region=${region:-1} + region=${region:-7} case "$region" in 1) do_region="ams2" ;; @@ -53,6 +53,59 @@ ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_t } +ec2 () { + read -p " +Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): +: " -rs aws_access_key + + read -p " +Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): +: " -rs aws_secret_key + + read -p " +Enter the local path to your SSH public key: +: " -r ssh_public_key + + read -p " +Name the vpn server: +[algo.local]: " -r aws_server_name + aws_server_name=${aws_server_name:-algo.local} + + read -p " + What region should the server be located in? + 1. us-east-1 US East (N. Virginia) + 2. us-west-1 US West (N. California) + 3. us-west-2 US West (Oregon) + 4. ap-south-1 Asia Pacific (Mumbai) + 5. ap-northeast-2 Asia Pacific (Seoul) + 6. ap-southeast-1 Asia Pacific (Singapore) + 7. ap-southeast-2 Asia Pacific (Sydney) + 8. ap-northeast-1 Asia Pacific (Tokyo) + 9. eu-central-1 EU (Frankfurt) + 10. eu-west-1 EU (Ireland) + 11. sa-east-1 South America (São Paulo) +Enter the number of your desired region: +[1]: " -r aws_region + aws_region=${aws_region:-1} + + case "$aws_region" in + 1) region="us-east-1" ;; + 2) region="us-west-1" ;; + 3) region="us-west-2" ;; + 4) region="ap-south-1" ;; + 5) region="ap-northeast-2" ;; + 6) region="ap-southeast-1" ;; + 7) region="ap-southeast-2" ;; + 8) region="ap-northeast-1" ;; + 9) region="eu-central-1" ;; + 10) region="eu-west-1" ;; + 11) region="sa-east-1" ;; + esac + +ansible-playbook deploy.yml -t ec2,vpn -e "aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" + +} + algo_provisioning () { echo -n " What provider would you like to use? @@ -68,13 +121,12 @@ Enter the number of your desired provider case "$N" in 1) digitalocean; ;; - 2) CLOUD="ec2" ;; + 2) ec2; ;; 3) CLOUD="gce" ;; 4) CLOUD="non-cloud" ;; *) exit 1 ;; esac - - ansible-playbook "${CLOUD}.yml" + } user_management () { From a470bf071e3a0bcbe9bd24428274aee6361f1d9f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 18 Sep 2016 14:03:20 +0300 Subject: [PATCH 117/769] GCE prompts --- algo | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/algo b/algo index fd5a012a..66fc1767 100755 --- a/algo +++ b/algo @@ -68,8 +68,8 @@ Enter the local path to your SSH public key: read -p " Name the vpn server: -[algo.local]: " -r aws_server_name - aws_server_name=${aws_server_name:-algo.local} +[algo]: " -r aws_server_name + aws_server_name=${aws_server_name:-algo} read -p " What region should the server be located in? @@ -106,6 +106,59 @@ ansible-playbook deploy.yml -t ec2,vpn -e "aws_access_key=$aws_access_key aws_se } +gce () { + read -p " +Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): +: " -r credentials_file + + read -p " +Enter the local path to your SSH public key: +: " -r ssh_public_key + + read -p " +Name the vpn server: +[algo]: " -r server_name + server_name=${server_name:-algo} + + read -p " + What zone should the server be located in? + 1. Central US (Iowa A) + 2. Central US (Iowa B) + 3. Central US (Iowa C) + 4. Central US (Iowa F) + 5. Eastern US (South Carolina B) + 6. Eastern US (South Carolina C) + 7. Eastern US (South Carolina D) + 8. Western Europe (Belgium B) + 9. Western Europe (Belgium C) + 10. Western Europe (Belgium D) + 11. East Asia (Taiwan A) + 12. East Asia (Taiwan B) + 13. East Asia (Taiwan C) +Please choose the number of your zone. Press enter for default (#8) zone. +[8]: " -r region + region=${region:-8} + + case "$region" in + 1) zone="us-central1-a" ;; + 2) zone="us-central1-b" ;; + 3) zone="us-central1-c" ;; + 4) zone="us-central1-f" ;; + 5) zone="us-east1-b" ;; + 6) zone="us-east1-c" ;; + 7) zone="us-east1-d" ;; + 8) zone="europe-west1-b" ;; + 9) zone="europe-west1-c" ;; + 10) zone="europe-west1-d" ;; + 11) zone="asia-east1-a" ;; + 12) zone="asia-east1-b" ;; + 13) zone="asia-east1-c" ;; + esac + +ansible-playbook deploy.yml -t gce,vpn -e "credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" + +} + algo_provisioning () { echo -n " What provider would you like to use? @@ -122,7 +175,7 @@ Enter the number of your desired provider case "$N" in 1) digitalocean; ;; 2) ec2; ;; - 3) CLOUD="gce" ;; + 3) gce; ;; 4) CLOUD="non-cloud" ;; *) exit 1 ;; esac From fc162728d3af40459dd59ac77e82770a6e2f9028 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 19 Sep 2016 19:54:45 +0300 Subject: [PATCH 118/769] role for local installation --- algo | 19 +++++++- deploy.yml | 3 +- non-cloud.yml | 89 ---------------------------------- roles/cloud-gce/tasks/main.yml | 2 +- roles/local/handlers/main.yml | 0 roles/local/tasks/main.yml | 12 +++++ 6 files changed, 33 insertions(+), 92 deletions(-) delete mode 100644 non-cloud.yml create mode 100644 roles/local/handlers/main.yml create mode 100644 roles/local/tasks/main.yml diff --git a/algo b/algo index 66fc1767..d5302b40 100755 --- a/algo +++ b/algo @@ -159,6 +159,23 @@ ansible-playbook deploy.yml -t gce,vpn -e "credentials_file=$credentials_file se } +non_cloud () { + read -p " +Enter IP address of your server: (use localhost for local installation) +: " -r server_ip + + read -p " +What user should we use to login on the server? (ignore if you're deploying to localhost) +[root]: " -r server_user + server_user=${server_user:-root} + + read -p " +Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) +: " -r IP_subject + + ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject=$IP_subject" +} + algo_provisioning () { echo -n " What provider would you like to use? @@ -176,7 +193,7 @@ Enter the number of your desired provider 1) digitalocean; ;; 2) ec2; ;; 3) gce; ;; - 4) CLOUD="non-cloud" ;; + 4) non_cloud; ;; *) exit 1 ;; esac diff --git a/deploy.yml b/deploy.yml index bca20ee3..81c6d845 100644 --- a/deploy.yml +++ b/deploy.yml @@ -7,7 +7,8 @@ roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } - { role: cloud-ec2, tags: ['ec2'] } - - { role: cloud-gce, tags: ['gce'] } + - { role: cloud-gce, tags: ['gce'] } + - { role: local, tags: ['local'] } - name: Post-provisioning tasks hosts: vpn-host diff --git a/non-cloud.yml b/non-cloud.yml deleted file mode 100644 index 8f5a33eb..00000000 --- a/non-cloud.yml +++ /dev/null @@ -1,89 +0,0 @@ -# vim:ft=ansible: -- hosts: localhost - gather_facts: False - vars_files: - - config.cfg - - vars_prompt: - - name: "server_ip" - prompt: "Enter IP address of your server: (use localhost for local installation)\n" - default: localhost - private: no - - - name: "server_user" - prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" - default: "root" - private: no - - - name: "dns_enabled" - prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" - default: "y" - private: no - - - name: "proxy_enabled" - prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" - default: "y" - private: no - - - name: "auditd_enabled" - prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" - default: "y" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "y" - private: no - - - name: "security_enabled" - prompt: "Do you want to enable the security role? (y/n):\n" - default: "y" - private: no - - - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - default: "vpnpw" - private: yes - - - name: "IP_subject" - prompt: "Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" - private: no - - tasks: - - name: Add the server to the vpn-host group - add_host: - hostname: "{{ server_ip }}" - groupname: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - dns_enabled: "{{ dns_enabled }}" - proxy_enabled: "{{ proxy_enabled }}" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - security_enabled: "{{ security_enabled }}" - auditd_enabled: " {{ auditd_enabled }}" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" - IP_subject: "{{ IP_subject }}" - -- name: Post-provisioning tasks - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - set_fact: - IP_subject_alt_name: "{{ IP_subject }}" - - roles: - - common - - { role: security, when: security_enabled is defined and security_enabled == "y" } - - { role: proxy, when: proxy_enabled is defined and proxy_enabled == "y" } - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == "y" } - - { role: ssh_tunneling, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - vpn diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 9c12f479..959ec6f0 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -16,7 +16,7 @@ - name: Add the instance to an inventory group add_host: - name: "{{ google_vm.instance_data[0].public_ip}}" + name: "{{ google_vm.instance_data[0].public_ip }}" groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" diff --git a/roles/local/handlers/main.yml b/roles/local/handlers/main.yml new file mode 100644 index 00000000..e69de29b diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml new file mode 100644 index 00000000..de8de4c0 --- /dev/null +++ b/roles/local/tasks/main.yml @@ -0,0 +1,12 @@ +- name: Add the instance to an inventory group + add_host: + name: "{{ server_ip }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: local + +- name: Waiting for SSH to become available + local_action: "wait_for port=22 host={{ server_ip }} timeout=320" + when: server_ip != "localhost" From 6441f2186bd8cbe8cb265992b3be9e70f9bd77b0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 19 Sep 2016 19:59:45 +0300 Subject: [PATCH 119/769] some README fixes --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 22924520..2dfc9774 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,12 @@ Ansible scripts are organized into roles. The roles used by Algo are described i - eu-west-1 - sa-east-1 +**local installation** +*Requirement variables:* +- server_ip +- server_user +- IP_subject + ### Cloud Deployment To install the dependencies on OS X or Linux: From 69e7f1e5dc1812ca25846faee3ce205eb2038737 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 19 Sep 2016 20:02:25 +0300 Subject: [PATCH 120/769] README fixes --- ADVANCED.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 77 ++------------------------------------------------ 2 files changed, 83 insertions(+), 75 deletions(-) create mode 100644 ADVANCED.md diff --git a/ADVANCED.md b/ADVANCED.md new file mode 100644 index 00000000..a9384f16 --- /dev/null +++ b/ADVANCED.md @@ -0,0 +1,81 @@ +### Cloud Providers + +**digitalocean** +*Requirement variables:* +- do_access_token +- do_ssh_name +- do_server_name +- do_region + +*Possible regions:* +- ams2 +- ams3 +- fra1 +- lon1 +- nyc1 +- nyc2 +- nyc3 +- sfo1 +- sfo2 +- sgp1 +- tor1 +- blr1 + +**gce** +*Requirement variables:* +- credentials_file +- server_name +- ssh_public_key +- zone + +*Possible zones:* +- us-central1-a +- us-central1-b +- us-central1-c +- us-central1-f +- us-east1-b +- us-east1-c +- us-east1-d +- europe-west1-b +- europe-west1-c +- europe-west1-d +- asia-east1-a +- asia-east1-b +- asia-east1-c + +**ec2** +*Requirement variables:* +- aws_access_key +- aws_secret_key +- aws_server_name +- ssh_public_key +- region + +*Possible regions:* +- us-east-1 +- us-west-1 +- us-west-2 +- ap-south-1 +- ap-northeast-2 +- ap-southeast-1 +- ap-southeast-2 +- ap-northeast-1 +- eu-central-1 +- eu-west-1 +- sa-east-1 + +**local installation** +*Requirement variables:* +- server_ip +- server_user +- IP_subject + +### Deployment + +Start the deploy with extra variables and tags that you need. +Example for DigitalOcean: + +``` +ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=secret_token_abc do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' +``` + diff --git a/README.md b/README.md index 2dfc9774..5cb7148f 100644 --- a/README.md +++ b/README.md @@ -87,78 +87,6 @@ Ansible scripts are organized into roles. The roles used by Algo are described i - role: security, tags: security - role: ssh_tunneling, tags: ssh_tunneling -### Cloud Providers - -**digitalocean** -*Requirement variables:* -- do_access_token -- do_ssh_name -- do_server_name -- do_region - -*Possible regions:* -- ams2 -- ams3 -- fra1 -- lon1 -- nyc1 -- nyc2 -- nyc3 -- sfo1 -- sfo2 -- sgp1 -- tor1 -- blr1 - -**gce** -*Requirement variables:* -- credentials_file -- server_name -- ssh_public_key -- zone - -*Possible zones:* -- us-central1-a -- us-central1-b -- us-central1-c -- us-central1-f -- us-east1-b -- us-east1-c -- us-east1-d -- europe-west1-b -- europe-west1-c -- europe-west1-d -- asia-east1-a -- asia-east1-b -- asia-east1-c - -**ec2** -*Requirement variables:* -- aws_access_key -- aws_secret_key -- aws_server_name -- ssh_public_key -- region - -*Possible regions:* -- us-east-1 -- us-west-1 -- us-west-2 -- ap-south-1 -- ap-northeast-2 -- ap-southeast-1 -- ap-southeast-2 -- ap-northeast-1 -- eu-central-1 -- eu-west-1 -- sa-east-1 - -**local installation** -*Requirement variables:* -- server_ip -- server_user -- IP_subject - ### Cloud Deployment To install the dependencies on OS X or Linux: @@ -170,11 +98,10 @@ sudo pip install -r requirements.txt Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -Start the deploy with extra variables and tags that you need. -Example for DigitalOcean: +Start the deploy and follow the instructions: ``` -ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=secret_token do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' +./algo ``` When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. From 4d731580b7bcb904e5802802ec86b2dfc0ba7fbf Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 19 Sep 2016 20:18:27 +0300 Subject: [PATCH 121/769] linting --- deploy.yml | 18 ++++---- playbooks/common.yml | 2 +- playbooks/digitalocean.yml | 80 +---------------------------------- roles/logging/tasks/main.yml | 4 +- roles/security/tasks/main.yml | 4 +- roles/vpn/handlers/main.yml | 2 +- roles/vpn/meta/main.yml | 2 +- 7 files changed, 17 insertions(+), 95 deletions(-) diff --git a/deploy.yml b/deploy.yml index 81c6d845..4e6ca209 100644 --- a/deploy.yml +++ b/deploy.yml @@ -3,26 +3,26 @@ tags: algo vars_files: - config.cfg - + roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } - - { role: cloud-ec2, tags: ['ec2'] } - - { role: cloud-gce, tags: ['gce'] } + - { role: cloud-ec2, tags: ['ec2'] } + - { role: cloud-gce, tags: ['gce'] } - { role: local, tags: ['local'] } - + - name: Post-provisioning tasks hosts: vpn-host gather_facts: false - tags: algo + tags: algo become: true vars_files: - config.cfg - pre_tasks: + pre_tasks: - name: Common pre-tasks include: playbooks/common.yml tags: [ 'digitalocean', 'ec2', 'gce', 'pre' ] - + - name: DigitalOcean pre-tasks include: playbooks/digitalocean.yml tags: [ 'digitalocean' ] @@ -33,9 +33,9 @@ - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - { role: logging, tags: [ 'logging' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - - { role: vpn, tags: [ 'vpn' ] } + - { role: vpn, tags: [ 'vpn' ] } handlers: - name: reload eth0 shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' - + diff --git a/playbooks/common.yml b/playbooks/common.yml index 1cf52830..d84a6eb0 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -1,5 +1,5 @@ - name: Install prerequisites raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - + - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 diff --git a/playbooks/digitalocean.yml b/playbooks/digitalocean.yml index 89734472..703e5d05 100644 --- a/playbooks/digitalocean.yml +++ b/playbooks/digitalocean.yml @@ -1,81 +1,3 @@ - #vars: - #regions: - #"1": "ams2" - #"2": "ams3" - #"3": "fra1" - #"4": "lon1" - #"5": "nyc1" - #"6": "nyc2" - #"7": "nyc3" - #"8": "sfo1" - #"9": "sfo2" - #"10": "sgp1" - #"11": "tor1" - #"12": "blr1" - - #vars_prompt: - #- name: "do_access_token" - #prompt: "Enter your API Token (https://cloud.digitalocean.com/settings/api/tokens):\n" - #private: yes - - #- name: "do_ssh_name" - #prompt: "Enter a valid SSH key name (https://cloud.digitalocean.com/settings/security):\n" - #private: no - - #- name: "do_region" - #prompt: > - #What region should the server be located in? - #1. Amsterdam (Datacenter 2) - #2. Amsterdam (Datacenter 3) - #3. Frankfurt - #4. London - #5. New York (Datacenter 1) - #6. New York (Datacenter 2) - #7. New York (Datacenter 3) - #8. San Francisco (Datacenter 1) - #9. San Francisco (Datacenter 2) - #10. Singapore - #11. Toronto - #12. Bangalore - #Enter the number of your desired region: - #default: "7" - #private: no - - #- name: "do_server_name" - #prompt: "Name the vpn server:\n" - #default: "algo.local" - #private: no - - #- name: "dns_enabled" - #prompt: "Do you want to install a local DNS resolver to block ads while surfing? (y/n):\n" - #default: "y" - #private: no - - #- name: "proxy_enabled" - #prompt: "Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? (y/n):\n" - #default: "y" - #private: no - - #- name: "auditd_enabled" - #prompt: "Do you want to use auditd for security monitoring (see config.cfg)? (y/n):\n" - #default: "y" - #private: no - - #- name: "ssh_tunneling_enabled" - #prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - #default: "y" - #private: no - - #- name: "security_enabled" - #prompt: "Do you want to enable the security role? (y/n):\n" - #default: "y" - #private: no - - #- name: "easyrsa_p12_export_password" - #prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - #default: "vpnpw" - #private: yes - - name: Enable IPv6 on the droplet uri: url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" @@ -111,4 +33,4 @@ - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" - become: false \ No newline at end of file + become: false diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml index 48ed4796..13b07391 100644 --- a/roles/logging/tasks/main.yml +++ b/roles/logging/tasks/main.yml @@ -15,7 +15,7 @@ - name: Enable services service: name=auditd enabled=yes - + # Rsyslog - name: Rsyslog configured @@ -29,4 +29,4 @@ - restart rsyslog - name: Enable services - service: name=rsyslog enabled=yes + service: name=rsyslog enabled=yes diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 6ad36c56..f9516169 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -2,12 +2,12 @@ apt: name="{{ item }}" state=latest with_items: - unattended-upgrades - + - name: Configure unattended-upgrades template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 - name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 + template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 - name: Find directories for minimizing access stat: diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index a8e921a4..fae797fb 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -6,7 +6,7 @@ - name: restart apparmor service: name=apparmor state=restarted - + - name: save iptables shell: service netfilter-persistent save diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index 149a6fbf..4b583d69 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -2,4 +2,4 @@ dependencies: - { role: common } - + From 94b3cc630c8e29874a0619fd8001e6a11ca6ed91 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 26 Sep 2016 15:43:09 +0300 Subject: [PATCH 122/769] #89 fixed --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index d5302b40..bf1ebe4d 100755 --- a/algo +++ b/algo @@ -173,7 +173,7 @@ What user should we use to login on the server? (ignore if you're deploying to l Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) : " -r IP_subject - ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject=$IP_subject" + ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" } algo_provisioning () { From 8e0cca6b666d03f05a725fd519022e298c085c52 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 26 Sep 2016 15:43:19 +0300 Subject: [PATCH 123/769] some fixes --- ADVANCED.md | 2 +- roles/local/tasks/main.yml | 12 ++++++++++++ roles/vpn/tasks/main.yml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ADVANCED.md b/ADVANCED.md index a9384f16..00de5fef 100644 --- a/ADVANCED.md +++ b/ADVANCED.md @@ -68,7 +68,7 @@ *Requirement variables:* - server_ip - server_user -- IP_subject +- IP_subject_alt_name ### Deployment diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index de8de4c0..4be24334 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -6,6 +6,18 @@ ansible_python_interpreter: "/usr/bin/python2.7" easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: local + when: server_ip != "localhost" + +- name: Add the instance to an inventory group + add_host: + name: "{{ server_ip }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_connection: local + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: local + when: server_ip == "localhost" - name: Waiting for SSH to become available local_action: "wait_for port=22 host={{ server_ip }} timeout=320" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index f658228a..3b9ea123 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -96,7 +96,7 @@ - name: Build the server pair shell: > - ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass&& + ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass && touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' From ad9d7d6ddbe0505a27cf81ffd3eeffd37132fae1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 26 Sep 2016 22:07:34 +0300 Subject: [PATCH 124/769] disable dpdtimeout #90 --- roles/vpn/templates/ipsec.conf.j2 | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index e0bec01d..b1dde994 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -5,14 +5,13 @@ config setup conn %default dpdaction=clear dpddelay=35s - dpdtimeout=300s rekey=no keyexchange=ikev2 ike=aes128gcm16-sha2_256-prfsha256-ecp256! esp=aes128gcm16-sha2_256-ecp256! compress=yes fragmentation=yes - + left=%any leftauth=pubkey leftid={{ IP_subject_alt_name }} @@ -26,9 +25,8 @@ conn %default {% if service_dns is defined and service_dns == "Y" %} rightdns={{ local_service_ip }} {% else %} - rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} -{% endif %} - + rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} +{% endif %} conn ikev2-pubkey - auto=add + auto=add From 105cb601e467a51aac829e2e2a96a003d6018f79 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 6 Oct 2016 20:39:53 +0300 Subject: [PATCH 125/769] Add the SSH role to the users-update playbook #92 fixed --- users.yml | 174 +++++++++++++++++++++++------------------------------- 1 file changed, 75 insertions(+), 99 deletions(-) diff --git a/users.yml b/users.yml index 6401dd53..6bdbf2e4 100644 --- a/users.yml +++ b/users.yml @@ -18,7 +18,7 @@ - name: "ssh_tunneling_enabled" prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "y" + default: "n" private: no - name: "easyrsa_p12_export_password" @@ -56,115 +56,91 @@ - set_fact: IP_subject_alt_name: "{{ IP_subject }}" + roles: + - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + tasks: - - name: Build the client's pair - shell: > - ./easyrsa build-client-full {{ item }} nopass && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' - with_items: "{{ users }}" + - name: Build the client's pair + shell: > + ./easyrsa build-client-full {{ item }} nopass && + touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + with_items: "{{ users }}" - - name: Build the client's p12 - shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' - with_items: "{{ users }}" + - name: Build the client's p12 + shell: > + openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && + touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + with_items: "{{ users }}" - - name: Get active users - shell: > - grep ^V pki/index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - register: valid_certs + - name: Get active users + shell: > + grep ^V pki/index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + register: valid_certs - - name: Revoke non-existing users - shell: > - ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && - ./easyrsa revoke {{ item }} && - ipsec rereadcrls - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" + - name: Revoke non-existing users + shell: > + ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && + ./easyrsa revoke {{ item }} && + ipsec rereadcrls + args: + chdir: '{{ easyrsa_dir }}/easyrsa3/' + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" - - name: Register p12 PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 - register: PayloadContent - with_items: "{{ users }}" + - name: Register p12 PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 + register: PayloadContent + with_items: "{{ users }}" - - name: Register CA PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 - register: PayloadContentCA + - name: Register CA PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 + register: PayloadContentCA - - name: Build the mobileconfigs - template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True + - name: Build the mobileconfigs + template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True - - name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes - with_items: "{{ users }}" + - name: Fetch users P12 + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes + with_items: "{{ users }}" - - name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes - with_items: "{{ users }}" + - name: Fetch users mobileconfig + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes + with_items: "{{ users }}" - - name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes + - name: Fetch server CA certificate + fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes - # SSH + # SSH - - name: SSH | Ensure that the system users exist - user: - name: "{{ item }}" - groups: algo - home: '/var/jail/{{ item }}' - createhome: yes - generate_ssh_key: yes - shell: /bin/false - ssh_key_type: rsa - ssh_key_bits: 2048 - ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" - state: present - append: yes - with_items: "{{ users }}" - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + - name: SSH | Get active system users + shell: > + getent group algo | cut -f4 -d: | sed "s/,/\n/g" + register: valid_users + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - - name: SSH | The authorized keys file created - file: - src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' - dest: '/var/jail/{{ item }}/.ssh/authorized_keys' - owner: "{{ item }}" - group: "{{ item }}" - state: link - with_items: "{{ users }}" - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + - name: SSH | Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + with_items: "{{ valid_users.stdout_lines }}" - - name: SSH | Get active system users - shell: > - getent group algo | cut -f4 -d: | sed "s/,/\n/g" - register: valid_users - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - - - name: SSH | Delete non-existing users - user: - name: "{{ item }}" - state: absent - remove: yes - force: yes - when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ valid_users.stdout_lines }}" - - - name: SSH | Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes - with_items: "{{ users }}" + - name: SSH | Fetch users SSH private keys + fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + with_items: "{{ users }}" From 2cca45c967fa776abbdca45e621c057c508af3cf Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 10 Oct 2016 15:32:14 +0300 Subject: [PATCH 126/769] additional tags --- roles/dns_adblocking/meta/main.yml | 2 +- roles/logging/meta/main.yml | 2 +- roles/proxy/meta/main.yml | 2 +- roles/security/meta/main.yml | 2 +- roles/ssh_tunneling/meta/main.yml | 2 +- roles/vpn/meta/main.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/roles/dns_adblocking/meta/main.yml b/roles/dns_adblocking/meta/main.yml index 325ccd93..e985f927 100644 --- a/roles/dns_adblocking/meta/main.yml +++ b/roles/dns_adblocking/meta/main.yml @@ -1,4 +1,4 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common, tags: common } diff --git a/roles/logging/meta/main.yml b/roles/logging/meta/main.yml index 325ccd93..e985f927 100644 --- a/roles/logging/meta/main.yml +++ b/roles/logging/meta/main.yml @@ -1,4 +1,4 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common, tags: common } diff --git a/roles/proxy/meta/main.yml b/roles/proxy/meta/main.yml index 325ccd93..e985f927 100644 --- a/roles/proxy/meta/main.yml +++ b/roles/proxy/meta/main.yml @@ -1,4 +1,4 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common, tags: common } diff --git a/roles/security/meta/main.yml b/roles/security/meta/main.yml index 325ccd93..e985f927 100644 --- a/roles/security/meta/main.yml +++ b/roles/security/meta/main.yml @@ -1,4 +1,4 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common, tags: common } diff --git a/roles/ssh_tunneling/meta/main.yml b/roles/ssh_tunneling/meta/main.yml index 325ccd93..e985f927 100644 --- a/roles/ssh_tunneling/meta/main.yml +++ b/roles/ssh_tunneling/meta/main.yml @@ -1,4 +1,4 @@ --- dependencies: - - { role: common } \ No newline at end of file + - { role: common, tags: common } diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index 4b583d69..f3d19204 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -1,5 +1,5 @@ --- dependencies: - - { role: common } + - { role: common, tags: common } From 4db428a86ec2aa451d8109e8d7b48d7a462eea73 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 10 Oct 2016 15:42:32 +0300 Subject: [PATCH 127/769] Disable unneeded plugins in StrongSwan #84 --- config.cfg | 22 +++++++++++++++++++++- roles/vpn/tasks/main.yml | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index cd827f35..4704920d 100644 --- a/config.cfg +++ b/config.cfg @@ -28,12 +28,32 @@ IP_subject_alt_name: "{{ ansible_ssh_host }}" # Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) service_dns: True -# If you don't want to use a local DNS resolver (option `service_dns`) you need to define DNS servers in this list. +# If you don't want to use a local DNS resolver (option `service_dns`) you need to define DNS servers in this list. dns_servers: - 8.8.8.8 - 8.8.4.4 - 2001:4860:4860::8888 - 2001:4860:4860::8844 +strongswan_enabled_plugins: + - aes + - gcm + - hmac + - kernel-netlink + - nonce + - openssl + - pem + - pgp + - pkcs12 + - pkcs7 + - pkcs8 + - pubkey + - random + - revocation + - sha2 + - socket-default + - stroke + - x509 + # IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 3b9ea123..690a44a0 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -66,6 +66,25 @@ notify: - restart strongswan +- name: Get loaded plugins + shell: > + find /etc/strongswan.d/charon/ -type f -name '*.conf' -printf '%f\n' | cut -f1 -d. + register: strongswan_plugins + +- name: Disable unneeded plugins + lineinfile: dest="/etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = no' state=present + notify: + - restart strongswan + when: item not in strongswan_enabled_plugins + with_items: "{{ strongswan_plugins.stdout_lines }}" + +- name: Ensure that required plugins are enabled + lineinfile: dest="/etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = yes' state=present + notify: + - restart strongswan + when: item in strongswan_enabled_plugins + with_items: "{{ strongswan_plugins.stdout_lines }}" + - name: Fetch easy-rsa-ipsec from git git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git version=ed4de10d7ce0726357fb1bb4729f8eb440c06e2b dest="{{ easyrsa_dir }}" From bff7c414b2ddf00316ac7f9ca9c7e46c2182a803 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Oct 2016 15:27:06 +0200 Subject: [PATCH 128/769] Initial commit of reorg'd docs --- ADVANCED.md | 81 -------------------- CONTRIBUTING.md | 4 +- README.md | 129 ++++++++++--------------------- algo | 30 ++++---- config.cfg | 3 +- deploy.yml | 1 - docs/ADVANCED.md | 135 +++++++++++++++++++++++++++++++++ docs/ROLES.md | 32 ++++++++ requirements.txt | 1 + roles/cloud-ec2/tasks/main.yml | 4 +- 10 files changed, 228 insertions(+), 192 deletions(-) delete mode 100644 ADVANCED.md create mode 100644 docs/ADVANCED.md create mode 100644 docs/ROLES.md diff --git a/ADVANCED.md b/ADVANCED.md deleted file mode 100644 index 00de5fef..00000000 --- a/ADVANCED.md +++ /dev/null @@ -1,81 +0,0 @@ -### Cloud Providers - -**digitalocean** -*Requirement variables:* -- do_access_token -- do_ssh_name -- do_server_name -- do_region - -*Possible regions:* -- ams2 -- ams3 -- fra1 -- lon1 -- nyc1 -- nyc2 -- nyc3 -- sfo1 -- sfo2 -- sgp1 -- tor1 -- blr1 - -**gce** -*Requirement variables:* -- credentials_file -- server_name -- ssh_public_key -- zone - -*Possible zones:* -- us-central1-a -- us-central1-b -- us-central1-c -- us-central1-f -- us-east1-b -- us-east1-c -- us-east1-d -- europe-west1-b -- europe-west1-c -- europe-west1-d -- asia-east1-a -- asia-east1-b -- asia-east1-c - -**ec2** -*Requirement variables:* -- aws_access_key -- aws_secret_key -- aws_server_name -- ssh_public_key -- region - -*Possible regions:* -- us-east-1 -- us-west-1 -- us-west-2 -- ap-south-1 -- ap-northeast-2 -- ap-southeast-1 -- ap-southeast-2 -- ap-northeast-1 -- eu-central-1 -- eu-west-1 -- sa-east-1 - -**local installation** -*Requirement variables:* -- server_ip -- server_user -- IP_subject_alt_name - -### Deployment - -Start the deploy with extra variables and tags that you need. -Example for DigitalOcean: - -``` -ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=secret_token_abc do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' -``` - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a1001f0..c3a7ac52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ ### Common Issues * Check that you're using at least Ansible 2.1 -* If installing to a local server, try using a fresh install -* Please review the [FAQ](https://github.com/trailofbits/algo#faq) in the readme +* If installing to a local server, use a fresh install of Ubuntu 16.04 +* Please review the [FAQ](https://github.com/trailofbits/algo#faq) ### Coding Guidelines diff --git a/README.md b/README.md index 5cb7148f..c1252134 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw ## Features -* Supports only IKEv2 -* Supports only a single cipher suite w/ AES GCM, SHA2 HMAC, and P-256 DH -* Generates mobileconfig profiles to auto-configure Apple devices +* Supports only IKEv2 w/ a single cipher suite: AES GCM, SHA2 HMAC, and P-256 DH +* Generates Apple Profiles to auto-configure iOS and macOS devices * Provides helper scripts to add and remove users * Blocks ads with a local DNS resolver and HTTP proxy (optional) +* Sets up limited SSH tunnels for each user (optional) * Based on current versions of Ubuntu and StrongSwan * Installs to DigitalOcean, Amazon EC2, Google Cloud Engine, or your own server @@ -23,120 +23,71 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw * Does not claim to provide anonymity or censorship avoidance * Does not claim to protect you from the [FSB](https://en.wikipedia.org/wiki/Federal_Security_Service), [MSS](https://en.wikipedia.org/wiki/Ministry_of_State_Security_(China)), [DGSE](https://en.wikipedia.org/wiki/Directorate-General_for_External_Security), or [FSM](https://en.wikipedia.org/wiki/Flying_Spaghetti_Monster) -## Included Roles +## Initial Setup -Ansible scripts are organized into roles. The roles used by Algo are described in detail below. - -### Required Roles - -* **Common** - * Installs several required packages and software updates, then reboots if necessary - * Configures network interfaces and enables packet forwarding on them -* **VPN** - * Installs [StrongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges - * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user - * Bundles the appropriate certificates into Apple mobileconfig profiles for each user - -### Optional Roles - -* **Security Enhancements** - * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied - * Modify operating system features like core dumps, kernel parameters, and SUID binaries to limit possible attacks - * Modifies SSH to use only modern ciphers and a seccomp sandbox, and restricts access to many legacy and unwanted features, like X11 forwarding and SFTP - * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) -* **Ad Blocking and Compression HTTP Proxy** - * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset - * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy - * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations -* **DNS Ad Blocking** - * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains - * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations -* **Security Monitoring and Logging** - * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents - * Emails aggregated Logs to a configured address on a regular basis -* **SSH Tunneling** - * Adds a restricted `algo` group to SSH with no shell access and limited forwarding options - * Creates one limited, local account per user and an SSH public key for each - -## Usage - -### Requirements - -* ansible >= 2.1 -* python >= 2.6 -* [dopy=0.3.5](https://github.com/Wiredcraft/dopy) -* [boto](https://github.com/boto/boto) -* [azure >= 0.7.1](https://github.com/Azure/azure-sdk-for-python) -* [apache-libcloud](https://github.com/apache/libcloud) -* [libcloud](https://curl.haxx.se/docs/caextract.html) (for Mac OS) -* [six](https://github.com/JioCloud/python-six) -* SHell or BASH -* libselinux-python (for RedHat based distros) - -### Roles and Tags -**Cloud roles:** -- role: cloud-digitalocean, tags: digitalocean -- role: cloud-ec2, tags: ec2 -- role: cloud-gce, tags: gce - -**Server roles:** -- role: vpn, tags: vpn -- role: dns_adblocking, tags: dns, adblock -- role: proxy, tags: proxy, adblock -- role: logging, tags: logging -- role: security, tags: security -- role: ssh_tunneling, tags: ssh_tunneling - -### Cloud Deployment - -To install the dependencies on OS X or Linux: +The easiest way to get an Algo server running is to let it setup a new virtual machine in the cloud for you. +1. Install the dependencies on OS X or Linux: ``` sudo easy_install pip sudo pip install -r requirements.txt ``` -Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. - -Start the deploy and follow the instructions: - +2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +3. Start the deploy and follow the instructions: ``` ./algo ``` -When the process is done, you can find `.mobileconfig` files and certificates in the `configs` directory. Send the `.mobileconfig` profile to users with Apple devices. Note that profile installation is supported over AirDrop. Do not send the mobileconfig file over plaintext (e.g., e-mail) since it contains the keys to access the VPN. For those using other clients, like Windows or Android, securely send them the X.509 certificates for the server and their user. +That's it! You now have an Algo VPN server on the internet. -### Local Deployment +Note: for local or scripted deployment instructions see the [Advanced Usage](/docs/ADVANCED.md) documentation. -It is possible to download Algo to your own Ubuntu server and run the scripts locally. You need to install ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA and using a PPA requires installing `software-properties-common`. tl;dr: -``` -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible -git clone https://github.com/trailofbits/algo -cd algo && ./algo -``` +## User Management -### User Management +### Configuration Files -If you want to add or delete users, update the `users` list in `config.cfg` and run the command: +After Algo finishes setting up the server, you can find all the certificates and configuration files that users will need in the `config` directory. Make sure to adequately secure and transmit these files since many contain private keys. +* [adsf].mobileconfig: Apple Configuration Profiles. These are all-in-one configuration files for iOS and macOS devices. Open them to a compatible device to fully configure the VPN. Note that they can be installed via AirDrop. +* asdf +* asdf + +### Adding or Removing Users + +Algo's own scripts can easily add and remove users from the VPN server. + +1. Update the `users` list in your `config.cfg` +2. Run the command: ``` ./algo update-users ``` +The Algo VPN server now only contains the users listed in the `config.cfg` file. + +## SSH Tunneling + +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg`. None of these user accounts will have shell access and their SSH tunneling options are limited. This was done to ensure that users have the least access required to tunnel through the server. + +Use the following command to SSH tunnel through the server: + +```asdf``` + +[explain the options] + ## FAQ -### Has this been audited? +### Has Algo been audited? -No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use algo at your own risk. +No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. ### Why aren't you using Tor? -The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). +The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). ### Why aren't you using Racoon, LibreSwan, or OpenSwan? -Raccoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for StrongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. StrongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. +Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for StrongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. StrongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. ### Why aren't you using a memory-safe or verified IKE daemon? @@ -148,4 +99,4 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? -Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free, Open, and HardenedBSD. Follow along on our progress in [this issue](https://github.com/trailofbits/algo/issues/35). +Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). diff --git a/algo b/algo index bf1ebe4d..8f406a68 100755 --- a/algo +++ b/algo @@ -4,11 +4,11 @@ set -e digitalocean () { read -p " -Enter your API Token (https://cloud.digitalocean.com/settings/api/tokens): +Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): : " -rs do_access_token read -p " -Enter a valid SSH key name (https://cloud.digitalocean.com/settings/security): +Enter an existing SSH key name (https://cloud.digitalocean.com/settings/security): : " -r do_ssh_name read -p " @@ -32,9 +32,9 @@ Name the vpn server: 12. Bangalore Enter the number of your desired region: [7]: " -r region - region=${region:-7} + region=${region:-7} - case "$region" in + case "$region" in 1) do_region="ams2" ;; 2) do_region="ams3" ;; 3) do_region="fra1" ;; @@ -49,22 +49,22 @@ Enter the number of your desired region: 12) do_region="blr1" ;; esac -ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" +ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" } ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): -: " -rs aws_access_key +[asdf...]: " -rs aws_access_key read -p " Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): -: " -rs aws_secret_key +[asdf...]: " -rs aws_secret_key read -p " Enter the local path to your SSH public key: -: " -r ssh_public_key +[adsf]: " -r ssh_public_key read -p " Name the vpn server: @@ -86,7 +86,7 @@ Name the vpn server: 11. sa-east-1 South America (São Paulo) Enter the number of your desired region: [1]: " -r aws_region - aws_region=${aws_region:-1} + aws_region=${aws_region:-1} case "$aws_region" in 1) region="us-east-1" ;; @@ -155,7 +155,7 @@ Please choose the number of your zone. Press enter for default (#8) zone. 13) zone="asia-east1-c" ;; esac -ansible-playbook deploy.yml -t gce,vpn -e "credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" +ansible-playbook deploy.yml -t gce,vpn -e "credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" } @@ -173,7 +173,7 @@ What user should we use to login on the server? (ignore if you're deploying to l Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) : " -r IP_subject - ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" + ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" } algo_provisioning () { @@ -184,7 +184,7 @@ algo_provisioning () { 3. Google Compute Engine 4. Install to existing Ubuntu server -Enter the number of your desired provider +Enter the number of your desired provider : " read -r N @@ -198,12 +198,12 @@ Enter the number of your desired provider esac } - + user_management () { ansible-playbook users.yml } case "$1" in - update-users) user_management ;; - *) algo_provisioning ;; + update-users) user_management ;; + *) algo_provisioning ;; esac diff --git a/config.cfg b/config.cfg index 4704920d..8fe25827 100644 --- a/config.cfg +++ b/config.cfg @@ -5,7 +5,7 @@ users: - dan - jack -# If you're using auditd for monitoring, add an email address to send logs +# Add an email address to send logs if you're using auditd for monitoring, auditd_action_mail_acct: email@example.com ### Advanced users only below this line ### @@ -15,7 +15,6 @@ easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 easyrsa_p12_export_password: vpnpws - # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False diff --git a/deploy.yml b/deploy.yml index 4e6ca209..9fbf792f 100644 --- a/deploy.yml +++ b/deploy.yml @@ -38,4 +38,3 @@ handlers: - name: reload eth0 shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' - diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md new file mode 100644 index 00000000..98770335 --- /dev/null +++ b/docs/ADVANCED.md @@ -0,0 +1,135 @@ +# Advanced Usage + +## Requirements + +Before you begin, make sure you have installed all the dependencies necessary for your use case. Algo depends on the software below and most of it will be installed via the `requirements.txt` file. + +* ansible >= 2.1 +* python >= 2.6 +* [dopy=0.3.5](https://github.com/Wiredcraft/dopy) +* [boto](https://github.com/boto/boto) +* [azure >= 0.7.1](https://github.com/Azure/azure-sdk-for-python) +* [apache-libcloud](https://github.com/apache/libcloud) +* [libcloud](https://curl.haxx.se/docs/caextract.html) (for Mac OS) +* [six](https://github.com/JioCloud/python-six) +* SHell or BASH +* libselinux-python (for RedHat based distros) + +## Local Deployment + +It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. + +tl;dr: + +``` +sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible +sudo apt-get update && sudo apt-get install ansible +git clone https://github.com/trailofbits/algo +cd algo && ./algo +``` + +## Scripted Deployment + +Example for DigitalOcean: + +``` +ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=my_secret_token do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' +``` + +### Roles + +Cloud roles: + +- role: cloud-digitalocean, tags: digitalocean +- role: cloud-ec2, tags: ec2 +- role: cloud-gce, tags: gce + +Server roles: + +- role: vpn, tags: vpn +- role: dns_adblocking, tags: dns, adblock +- role: proxy, tags: proxy, adblock +- role: logging, tags: logging +- role: security, tags: security +- role: ssh_tunneling, tags: ssh_tunneling + +### Digital Ocean + +Required variables: + +- do_access_token +- do_ssh_name +- do_server_name +- do_region + +Possible regions: + +- ams2 +- ams3 +- fra1 +- lon1 +- nyc1 +- nyc2 +- nyc3 +- sfo1 +- sfo2 +- sgp1 +- tor1 +- blr1 + +### Google Cloud Engine + +Required variables: + +- credentials_file +- server_name +- ssh_public_key +- zone + +Possible zones: + +- us-central1-a +- us-central1-b +- us-central1-c +- us-central1-f +- us-east1-b +- us-east1-c +- us-east1-d +- europe-west1-b +- europe-west1-c +- europe-west1-d +- asia-east1-a +- asia-east1-b +- asia-east1-c + +### Amazon EC2 + +Required variables: + +- aws_access_key +- aws_secret_key +- aws_server_name +- ssh_public_key +- region + +Possible regions: + +- us-east-1 +- us-west-1 +- us-west-2 +- ap-south-1 +- ap-northeast-2 +- ap-southeast-1 +- ap-southeast-2 +- ap-northeast-1 +- eu-central-1 +- eu-west-1 +- sa-east-1 + +### Local Installation + +Required variables: + +- server_ip +- server_user +- IP_subject_alt_name \ No newline at end of file diff --git a/docs/ROLES.md b/docs/ROLES.md new file mode 100644 index 00000000..8e1df28b --- /dev/null +++ b/docs/ROLES.md @@ -0,0 +1,32 @@ +# Ansible Roles + +## Required Roles + +* **Common** + * Installs several required packages and software updates, then reboots if necessary + * Configures network interfaces and enables packet forwarding on them +* **VPN** + * Installs [StrongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges + * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user + * Bundles the appropriate certificates into Apple mobileconfig profiles for each user + * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) + +## Optional Roles + +* **Security Enhancements (Reccommended)** + * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied + * Modify features like core dumps, kernel parameters, and SUID binaries to limit possible attacks + * Enhances SSH with modern ciphers and seccomp, and restricts access to older, unwanted features like X11 forwarding and SFTP +* **Ad Blocking and Compression HTTP Proxy** + * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset + * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy + * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations +* **DNS Ad Blocking** + * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains + * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations +* **Security Monitoring and Logging** + * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents + * Sends logs to a configured email address on a regular basis +* **SSH Tunneling** + * Adds a restricted `algo` group with no shell access and limited SSH forwarding options + * Creates one limited, local account per user and an SSH public key for each diff --git a/requirements.txt b/requirements.txt index a666d82a..36b226c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +ansible>=2.1 dopy==0.3.5 boto azure>=0.7.1 diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 4f25e1b0..eace8c4d 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,4 +1,4 @@ -- name: Locate official Ubuntu 16.04 AMI for region. +- name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" @@ -13,7 +13,7 @@ - set_fact: ami_image: "{{ ami_search.results[0].ami_id }}" -- name: Add ssh public key. +- name: Add ssh public key ec2_key: aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" From e99d5dffea1d0221e23c63d77e10d6c15bc7c529 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Oct 2016 15:50:24 +0200 Subject: [PATCH 129/769] better defaults --- algo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/algo b/algo index 8f406a68..c0cec1d7 100755 --- a/algo +++ b/algo @@ -56,15 +56,15 @@ ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_t ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): -[asdf...]: " -rs aws_access_key +[AKIA...]: " -rs aws_access_key read -p " Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): -[asdf...]: " -rs aws_secret_key +[ABCD...]: " -rs aws_secret_key read -p " Enter the local path to your SSH public key: -[adsf]: " -r ssh_public_key +[~/.ssh/id_rsa.pub]: " -r ssh_public_key read -p " Name the vpn server: From 462bf29e08110b7244c2fe92267eb95e4b20f15b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Oct 2016 16:35:32 +0200 Subject: [PATCH 130/769] change EC2 instructions --- algo | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/algo b/algo index c0cec1d7..0e891da2 100755 --- a/algo +++ b/algo @@ -55,16 +55,18 @@ ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_t ec2 () { read -p " -Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): +Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [AKIA...]: " -rs aws_access_key read -p " -Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): +Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [ABCD...]: " -rs aws_secret_key read -p " Enter the local path to your SSH public key: -[~/.ssh/id_rsa.pub]: " -r ssh_public_key +: " -r ssh_public_key read -p " Name the vpn server: From fe1fcade72813e18a19b62369d62ecb63ae9be24 Mon Sep 17 00:00:00 2001 From: Defunct Date: Thu, 13 Oct 2016 14:45:41 +0000 Subject: [PATCH 131/769] resolves #99 --- algo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo b/algo index bf1ebe4d..d1e5773f 100755 --- a/algo +++ b/algo @@ -62,9 +62,9 @@ Enter your aws_access_key (http://docs.aws.amazon.com/AWSSimpleQueueService/late Enter your aws_secret_key (http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html): : " -rs aws_secret_key - read -p " + read -e -p " Enter the local path to your SSH public key: -: " -r ssh_public_key +: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key read -p " Name the vpn server: From c43ccc38987161f1e1028f94c4432ddec22a9e4e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Oct 2016 18:50:24 +0300 Subject: [PATCH 132/769] iptables moved to the vpn role #61 --- roles/security/handlers/main.yml | 3 --- roles/security/tasks/main.yml | 8 -------- roles/vpn/handlers/main.yml | 6 +++--- roles/vpn/tasks/iptables.yml | 9 +++++++++ roles/vpn/tasks/main.yml | 4 ++++ roles/{security => vpn}/templates/rules.v4.j2 | 0 roles/{security => vpn}/templates/rules.v6.j2 | 0 7 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 roles/vpn/tasks/iptables.yml rename roles/{security => vpn}/templates/rules.v4.j2 (100%) rename roles/{security => vpn}/templates/rules.v6.j2 (100%) diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index e79c49c0..e6d614b7 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,8 +1,5 @@ - name: restart ssh service: name=ssh state=restarted -- name: restart iptables - service: name=netfilter-persistent state=restarted - - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index f9516169..aed75763 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -88,14 +88,6 @@ - name: Do not send ICMP redirects (we are not a router) sysctl: name=net.ipv4.conf.all.send_redirects value=0 -- name: Iptables configured - template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 - with_items: - - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } - - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } - notify: - - restart iptables - - name: SSH config template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 notify: diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 4ba51734..84e08b04 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -6,13 +6,13 @@ - name: restart apparmor service: name=apparmor state=restarted - -- name: save iptables - shell: service netfilter-persistent save - name: save iptables shell: service netfilter-persistent save +- name: restart iptables + service: name=netfilter-persistent state=restarted + - name: congrats debug: msg: diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml new file mode 100644 index 00000000..aeed994b --- /dev/null +++ b/roles/vpn/tasks/iptables.yml @@ -0,0 +1,9 @@ +--- + +- name: Iptables configured + template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 + with_items: + - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } + - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } + notify: + - restart iptables diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 690a44a0..10099118 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -191,3 +191,7 @@ fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes notify: - congrats + +- include: iptables.yml + tags: iptables + diff --git a/roles/security/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 similarity index 100% rename from roles/security/templates/rules.v4.j2 rename to roles/vpn/templates/rules.v4.j2 diff --git a/roles/security/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 similarity index 100% rename from roles/security/templates/rules.v6.j2 rename to roles/vpn/templates/rules.v6.j2 From d8478e1741b01dfe0288e26a0945fcc0f0b81da4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Oct 2016 19:57:11 +0400 Subject: [PATCH 133/769] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5cb7148f..80edf404 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ Ansible scripts are organized into roles. The roles used by Algo are described i ## Usage +### Warning +If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwite the rules, just skip the `iptables` tag. (You can find some information about tags [here](https://github.com/trailofbits/algo/blob/master/ADVANCED.md)) + ### Requirements * ansible >= 2.1 From bf5d5e53acc1f9eadf8b330f0b1f1e0fb1e7107a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Oct 2016 19:05:39 +0300 Subject: [PATCH 134/769] ip6tables fixes --- roles/vpn/templates/rules.v6.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index e491fec5..71342a0d 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -17,6 +17,10 @@ COMMIT -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +-A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT +-A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT +-A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT +-A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any From 787929deb7f118815f74047e45a89b84740661b0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Oct 2016 19:26:30 +0300 Subject: [PATCH 135/769] fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cb7148f..e1472532 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Ansible scripts are organized into roles. The roles used by Algo are described i ### Roles and Tags **Cloud roles:** - role: cloud-digitalocean, tags: digitalocean -- role: cloud-ec2, tags: ec2 +- role: cloud-ec2, tags: ec2 - role: cloud-gce, tags: gce **Server roles:** From fcf29534bac957c5bba2ee04f835b27f8b8f1704 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Oct 2016 19:58:55 +0300 Subject: [PATCH 136/769] the proxixy filter rules disabled #93 --- roles/proxy/tasks/main.yml | 5 ++++- roles/proxy/templates/default.filter.j2 | 0 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 roles/proxy/templates/default.filter.j2 diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index 81dbcab1..fc3af8b9 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -5,7 +5,10 @@ apt: name=privoxy state=latest - name: Privoxy configured - template: src=privoxy_config.j2 dest=/etc/privoxy/config + template: src="{{ item.src }}" dest="{{ item.dest }}" + with_items: + - { src: privoxy_config.j2, dest: /etc/privoxy/config } + - { src: default.filter.j2, dest: /etc/privoxy/default.filter } notify: - restart privoxy diff --git a/roles/proxy/templates/default.filter.j2 b/roles/proxy/templates/default.filter.j2 new file mode 100644 index 00000000..e69de29b From f76c2690024268ee9fcdce2ace15a953718e4b96 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 15 Oct 2016 19:31:22 +0200 Subject: [PATCH 137/769] reorganize the readme to be even simpler --- README.md | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3a28e3c9..3e832b03 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw * Does not claim to provide anonymity or censorship avoidance * Does not claim to protect you from the [FSB](https://en.wikipedia.org/wiki/Federal_Security_Service), [MSS](https://en.wikipedia.org/wiki/Ministry_of_State_Security_(China)), [DGSE](https://en.wikipedia.org/wiki/Directorate-General_for_External_Security), or [FSM](https://en.wikipedia.org/wiki/Flying_Spaghetti_Monster) -## Initial Setup +## Deploy the Algo Server The easiest way to get an Algo server running is to let it setup a new virtual machine in the cloud for you. @@ -34,18 +34,13 @@ sudo pip install -r requirements.txt ``` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -3. Start the deploy and follow the instructions: -``` -./algo -``` +3. Start the deploy and follow the instructions: `./algo` That's it! You now have an Algo VPN server on the internet. Note: for local or scripted deployment instructions see the [Advanced Usage](/docs/ADVANCED.md) documentation. -## User Management - -### Configuration Files +## Configure the VPN Clients After Algo finishes setting up the server, you can find all the certificates and configuration files that users will need in the `config` directory. Make sure to adequately secure and transmit these files since many contain private keys. @@ -53,19 +48,7 @@ After Algo finishes setting up the server, you can find all the certificates and * asdf * asdf -### Adding or Removing Users - -Algo's own scripts can easily add and remove users from the VPN server. - -1. Update the `users` list in your `config.cfg` -2. Run the command: -``` -./algo update-users -``` - -The Algo VPN server now only contains the users listed in the `config.cfg` file. - -## SSH Tunneling +## Setup an SSH Tunnel If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg`. None of these user accounts will have shell access and their SSH tunneling options are limited. This was done to ensure that users have the least access required to tunnel through the server. @@ -75,6 +58,15 @@ Use the following command to SSH tunnel through the server: asdf then explain the options used +## Adding or Removing Users + +Algo's own scripts can easily add and remove users from the VPN server. + +1. Update the `users` list in your `config.cfg` +2. Run the command: `./algo update-users` + +The Algo VPN server now only contains the users listed in the `config.cfg` file. + ## FAQ ### Has Algo been audited? From 062426e0ecffab6bcabe3f8bfaab1f8382408b15 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Oct 2016 15:27:05 +0300 Subject: [PATCH 138/769] client configuration templates #43 --- config.cfg | 10 ++++++++ roles/vpn/tasks/main.yml | 26 +++++++++++++++++++++ roles/vpn/templates/client_ipsec.conf.j2 | 17 ++++++++++++++ roles/vpn/templates/client_ipsec.secrets.j2 | 2 ++ roles/vpn/templates/ipsec.conf.j2 | 11 +++------ 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 roles/vpn/templates/client_ipsec.conf.j2 create mode 100644 roles/vpn/templates/client_ipsec.secrets.j2 diff --git a/config.cfg b/config.cfg index 4704920d..e1052adb 100644 --- a/config.cfg +++ b/config.cfg @@ -55,5 +55,15 @@ strongswan_enabled_plugins: - stroke - x509 +ipsec_config: + dpdaction: 'clear' + dpddelay: '35s' + rekey: 'no' + keyexchange: 'ikev2' + ike: 'aes128gcm16-sha2_256-prfsha256-ecp256!' + esp: 'aes128gcm16-sha2_256-ecp256!' + compress: 'yes' + fragmentation: 'yes' + # IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 10099118..b152c7a2 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -174,6 +174,16 @@ - "{{ PayloadContent.results }}" no_log: True +- name: Build the client ipsec config file + template: src=client_ipsec.conf.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.conf mode=0600 + with_items: + - "{{ users }}" + +- name: Build the client ipsec secret file + template: src=client_ipsec.secrets.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.secrets mode=0600 + with_items: + - "{{ users }}" + - name: Fetch users P12 fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" @@ -182,6 +192,22 @@ fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes with_items: "{{ users }}" +- name: Fetch users certificates + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt dest=configs/{{ IP_subject_alt_name }}_{{ item }}.crt flat=yes + with_items: "{{ users }}" + +- name: Fetch users keys + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key dest=configs/{{ IP_subject_alt_name }}_{{ item }}.key flat=yes + with_items: "{{ users }}" + +- name: Fetch users ipsec configs + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.conf dest=configs/{{ IP_subject_alt_name }}_{{ item }}_ipsec.conf flat=yes + with_items: "{{ users }}" + +- name: Fetch users ipsec secrets + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.secrets dest=configs/{{ IP_subject_alt_name }}_{{ item }}_ipsec.secrets flat=yes + with_items: "{{ users }}" + - name: Restrict permissions file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root with_items: diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 new file mode 100644 index 00000000..3b01ff16 --- /dev/null +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -0,0 +1,17 @@ +conn ikev2-{{ IP_subject_alt_name }} +{% for key, value in ipsec_config.iteritems() %} + {{ key }}={{ value }} +{% endfor %} + + right={{ IP_subject_alt_name }} + rightid={{ IP_subject_alt_name }} + rightsubnet=0.0.0.0/0 + rightauth=pubkey + + leftsourceip=%config + leftauth=pubkey + leftcert={{ IP_subject_alt_name }}_{{ item }}.crt + leftfirewall=yes + left=%defaultroute + + auto=add diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/vpn/templates/client_ipsec.secrets.j2 new file mode 100644 index 00000000..ec4a30fa --- /dev/null +++ b/roles/vpn/templates/client_ipsec.secrets.j2 @@ -0,0 +1,2 @@ +{{ IP_subject_alt_name }} : ECDSA {{ IP_subject_alt_name }}_{{ item }}.key + diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index b1dde994..fa29458d 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -3,14 +3,9 @@ config setup charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" conn %default - dpdaction=clear - dpddelay=35s - rekey=no - keyexchange=ikev2 - ike=aes128gcm16-sha2_256-prfsha256-ecp256! - esp=aes128gcm16-sha2_256-ecp256! - compress=yes - fragmentation=yes +{% for key, value in ipsec_config.iteritems() %} + {{ key }}={{ value }} +{% endfor %} left=%any leftauth=pubkey From 8c284a16e3890551409cd2698cb36ddbbd55bc9d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Oct 2016 17:36:01 +0300 Subject: [PATCH 139/769] Done. #96 --- roles/proxy/tasks/main.yml | 26 ++++++++++++++++++++++++++ roles/vpn/tasks/main.yml | 4 ++++ roles/vpn/templates/mobileconfig.j2 | 22 +++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index fc3af8b9..e1d8b9d3 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -84,3 +84,29 @@ - restart apache2 - meta: flush_handlers + +- name: Set facts for mobileconfigs + set_fact: + proxy_enabled: true + +- name: Register p12 PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 + register: PayloadContent + with_items: "{{ users }}" + +- name: Register CA PayloadContent + shell: > + cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 + register: PayloadContentCA + +- name: Build the mobileconfigs + template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}_proxy.mobileconfig mode=0600 + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True + +- name: Fetch users mobileconfig + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}_proxy.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}_proxy.mobileconfig flat=yes + with_items: "{{ users }}" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index b152c7a2..fbe4b94e 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -167,6 +167,10 @@ cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 register: PayloadContentCA +- name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + - name: Build the mobileconfigs template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 with_together: diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 3fc3668b..be5b0718 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -76,12 +76,24 @@ Proxies HTTPEnable - 0 +{% if proxy_enabled is defined and proxy_enabled == true %} + 1 + HTTPPort + 8118 + HTTPProxy + {{ local_service_ip }} + {% else %} + 1 +{% endif %} HTTPSEnable 0 UserDefinedName +{% if proxy_enabled is defined and proxy_enabled == true %} + {{ IP_subject_alt_name }} IKEv2 with proxy + {% else %} {{ IP_subject_alt_name }} IKEv2 +{% endif %} VPNType IKEv2 @@ -129,9 +141,17 @@ PayloadDisplayName +{% if proxy_enabled is defined and proxy_enabled == true %} + {{ IP_subject_alt_name }} IKEv2 with proxy + {% else %} {{ IP_subject_alt_name }} IKEv2 +{% endif %} PayloadIdentifier +{% if proxy_enabled is defined and proxy_enabled == true %} + donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BA + {% else %} donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BE +{% endif %} PayloadRemovalDisallowed PayloadType From 0e613f2ff72a0bfb0d7b3004068345fe781f48f3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Oct 2016 17:38:00 +0300 Subject: [PATCH 140/769] fix a typo. #96 closed --- roles/vpn/templates/mobileconfig.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index be5b0718..5714839f 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -83,7 +83,7 @@ HTTPProxy {{ local_service_ip }} {% else %} - 1 + 0 {% endif %} HTTPSEnable 0 From d62b302b8f995a4c7dfaecfedce9aa0bc986bf6a Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Oct 2016 17:47:35 +0200 Subject: [PATCH 141/769] better contributor guidelines --- CONTRIBUTING.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3a7ac52..8074f82a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,14 @@ -### Common Issues +### Troubleshooting -* Check that you're using at least Ansible 2.1 +* Check that you installed all the dependencies with pip and have Ansible 2.1+ * If installing to a local server, use a fresh install of Ubuntu 16.04 + +### Filing New Issues + * Please review the [FAQ](https://github.com/trailofbits/algo#faq) +* Please include the full output from your terminal window if appropriate -### Coding Guidelines +### Pull Requests -* Please review any Pull Requests with [ansible-lint](https://github.com/willthames/ansible-lint) +* Run [ansible-lint](https://github.com/willthames/ansible-lint) on any new ansible scripts +* Run [shellcheck](https://github.com/koalaman/shellcheck) on any new shell scripts From d93b7c200f74705a1f072d4410464a9d0c0ee03e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Oct 2016 19:24:04 +0300 Subject: [PATCH 142/769] EC2 | Add VPC group #98 and counts #59 --- config.cfg | 4 ++++ roles/cloud-ec2/tasks/main.yml | 21 +++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/config.cfg b/config.cfg index e1052adb..22a48f8f 100644 --- a/config.cfg +++ b/config.cfg @@ -65,5 +65,9 @@ ipsec_config: compress: 'yes' fragmentation: 'yes' +ec2_vpc_nets: + cidr_block: 172.251.0.0/23 + subnet_cidr: 172.251.1.0/24 + # IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 4f25e1b0..3373614e 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -23,13 +23,25 @@ with_file: "{{ ssh_public_key }}" register: keypair +- name: Configure EC2 virtual private clouds + ec2_vpc: + state: present + resource_tags: { "Environment":"Algo" } + region: "{{ region }}" + cidr_block: "{{ ec2_vpc_nets.cidr_block }}" + subnets: + - cidr: "{{ ec2_vpc_nets.subnet_cidr }}" + resource_tags: { "Environment":"Algo" } + register: vpc + - name: Configure EC2 security group ec2_group: aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" - name: vpn-secgroup + name: algo-secgroup description: Security group for VPN servers region: "{{ region }}" + vpc_id: "{{ vpc.vpc_id }}" rules: - proto: udp from_port: 4500 @@ -54,13 +66,18 @@ aws_access_key: "{{ aws_access_key }}" aws_secret_key: "{{ aws_secret_key }}" keypair: "VPNKEY" - group: vpn-secgroup + group: algo-secgroup + vpc_subnet_id: "{{ vpc.subnets[0].id }}" instance_type: t2.nano image: "{{ ami_image }}" wait: true region: "{{ region }}" instance_tags: name: "{{ aws_server_name }}" + exact_count: 1 + count_tag: + name: "{{ aws_server_name }}" + assign_public_ip: yes register: ec2 - name: Add new instance to host group From 8ae80788ada7a7da9e9e14351cf2debecd2d0b8c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Oct 2016 23:05:20 +0200 Subject: [PATCH 143/769] better user instructions --- README.md | 29 ++++++++++++++++++++--------- config.cfg | 5 ++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3e832b03..e7468889 100644 --- a/README.md +++ b/README.md @@ -42,21 +42,32 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do ## Configure the VPN Clients -After Algo finishes setting up the server, you can find all the certificates and configuration files that users will need in the `config` directory. Make sure to adequately secure and transmit these files since many contain private keys. +After Algo finishes setting up the server, you can find all the certificates and configuration files that users will need in the `config` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. -* [adsf].mobileconfig: Apple Configuration Profiles. These are all-in-one configuration files for iOS and macOS devices. Open them to a compatible device to fully configure the VPN. Note that they can be installed via AirDrop. -* asdf -* asdf +### Apple Devices + +Find the corresponding mobileconfig (Apple Profile) for the user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. Installing a profile will fully configure the VPN. + +### StrongSwan Clients (e.g., OpenWRT) + +Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. + +### Other Devices + +* ca.crt: CA Certificate +* user_ipsec.conf: StrongSwan client configuration +* user_ipsec.secrets: StrongSwan client configuration +* user.crt: User Certificate +* user.key: User Private Key +* user.mobileconfig: Apple Profile +* user.p12: User Certificate and Private Key (in PKCS#12 format) +* user.ssh.pem (optional): SSH authorized_key file ## Setup an SSH Tunnel If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg`. None of these user accounts will have shell access and their SSH tunneling options are limited. This was done to ensure that users have the least access required to tunnel through the server. -Use the following command to SSH tunnel through the server: - -```asdf``` - -asdf then explain the options used +Make sure to access the server using 'ssh -N' for any limited accounts. ## Adding or Removing Users diff --git a/config.cfg b/config.cfg index e6c6b9a0..c9cedd81 100644 --- a/config.cfg +++ b/config.cfg @@ -8,12 +8,15 @@ users: # Add an email address to send logs if you're using auditd for monitoring, auditd_action_mail_acct: email@example.com +# Exported certificates will be protected by the password below: +easyrsa_p12_export_password: vpnpws + + ### Advanced users only below this line ### easyrsa_dir: /opt/easy-rsa-ipsec easyrsa_ca_expire: 3650 easyrsa_cert_expire: 3650 -easyrsa_p12_export_password: vpnpws # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False From c87c9f8f0e2b976c27eaf0d6b6f79c19886238eb Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 17 Oct 2016 16:08:39 +0200 Subject: [PATCH 144/769] easier to read --- README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e7468889..6d1e1ce9 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw The easiest way to get an Algo server running is to let it setup a new virtual machine in the cloud for you. -1. Install the dependencies on OS X or Linux: -``` -sudo easy_install pip -sudo pip install -r requirements.txt -``` - +1. Install the dependencies on OS X or Linux: `sudo easy_install pip && sudo pip install -r requirements.txt` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 3. Start the deploy and follow the instructions: `./algo` @@ -42,18 +37,20 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do ## Configure the VPN Clients -After Algo finishes setting up the server, you can find all the certificates and configuration files that users will need in the `config` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. +Certificates and configuration files that users will need are placed in the `config` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. ### Apple Devices -Find the corresponding mobileconfig (Apple Profile) for the user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. Installing a profile will fully configure the VPN. +Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices and installing a profile will fully configure the VPN. ### StrongSwan Clients (e.g., OpenWRT) -Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. +Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. These may be useful if you plan to set up a point-to-point VPN with OpenWRT or other custom device. ### Other Devices +Depending on the platform, you may need one or multiple of the following files. + * ca.crt: CA Certificate * user_ipsec.conf: StrongSwan client configuration * user_ipsec.secrets: StrongSwan client configuration @@ -61,13 +58,12 @@ Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificat * user.key: User Private Key * user.mobileconfig: Apple Profile * user.p12: User Certificate and Private Key (in PKCS#12 format) -* user.ssh.pem (optional): SSH authorized_key file ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg`. None of these user accounts will have shell access and their SSH tunneling options are limited. This was done to ensure that users have the least access required to tunnel through the server. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key file will be in the `config` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited. This is done to ensure that users have the least access required to tunnel through the server. -Make sure to access the server using 'ssh -N' for any limited accounts. +Make sure to access the server using 'ssh -N' with these limited accounts. ## Adding or Removing Users From ff66be9ba82109680f8c0d524f6d477aa85b0603 Mon Sep 17 00:00:00 2001 From: defunct Date: Mon, 17 Oct 2016 13:07:54 -0400 Subject: [PATCH 145/769] #99 also --- algo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo b/algo index 3ee1bc4f..ec8da23d 100755 --- a/algo +++ b/algo @@ -113,9 +113,9 @@ gce () { Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): : " -r credentials_file - read -p " + read -e -p " Enter the local path to your SSH public key: -: " -r ssh_public_key +: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key read -p " Name the vpn server: From 46e5e0aa33142be3d12007a996f9f9a6910cbcc0 Mon Sep 17 00:00:00 2001 From: defunct Date: Tue, 18 Oct 2016 10:35:34 -0400 Subject: [PATCH 146/769] Add new Ohio region --- docs/ADVANCED.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 2bd50f26..ad057e8e 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -117,6 +117,7 @@ Required variables: Possible regions: - us-east-1 +- us-east-2 - us-west-1 - us-west-2 - ap-south-1 @@ -134,4 +135,4 @@ Required variables: - server_ip - server_user -- IP_subject_alt_name \ No newline at end of file +- IP_subject_alt_name From 5769d5a1cc362b13915ee099a648b08d9614bb8f Mon Sep 17 00:00:00 2001 From: defunct Date: Tue, 18 Oct 2016 10:39:12 -0400 Subject: [PATCH 147/769] Add EC2 Ohio region --- algo | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/algo b/algo index ec8da23d..6921beff 100755 --- a/algo +++ b/algo @@ -76,32 +76,34 @@ Name the vpn server: read -p " What region should the server be located in? 1. us-east-1 US East (N. Virginia) - 2. us-west-1 US West (N. California) - 3. us-west-2 US West (Oregon) - 4. ap-south-1 Asia Pacific (Mumbai) - 5. ap-northeast-2 Asia Pacific (Seoul) - 6. ap-southeast-1 Asia Pacific (Singapore) - 7. ap-southeast-2 Asia Pacific (Sydney) - 8. ap-northeast-1 Asia Pacific (Tokyo) - 9. eu-central-1 EU (Frankfurt) - 10. eu-west-1 EU (Ireland) - 11. sa-east-1 South America (São Paulo) + 2. us-east-2 US East (Ohio) + 3. us-west-1 US West (N. California) + 4. us-west-2 US West (Oregon) + 5. ap-south-1 Asia Pacific (Mumbai) + 6. ap-northeast-2 Asia Pacific (Seoul) + 7. ap-southeast-1 Asia Pacific (Singapore) + 8. ap-southeast-2 Asia Pacific (Sydney) + 9. ap-northeast-1 Asia Pacific (Tokyo) + 10. eu-central-1 EU (Frankfurt) + 11. eu-west-1 EU (Ireland) + 12. sa-east-1 South America (São Paulo) Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} case "$aws_region" in 1) region="us-east-1" ;; - 2) region="us-west-1" ;; - 3) region="us-west-2" ;; - 4) region="ap-south-1" ;; - 5) region="ap-northeast-2" ;; - 6) region="ap-southeast-1" ;; - 7) region="ap-southeast-2" ;; - 8) region="ap-northeast-1" ;; - 9) region="eu-central-1" ;; - 10) region="eu-west-1" ;; - 11) region="sa-east-1" ;; + 2) region="us-east-2" ;; + 3) region="us-west-1" ;; + 4) region="us-west-2" ;; + 5) region="ap-south-1" ;; + 6) region="ap-northeast-2" ;; + 7) region="ap-southeast-1" ;; + 8) region="ap-southeast-2" ;; + 9) region="ap-northeast-1" ;; + 10) region="eu-central-1" ;; + 11) region="eu-west-1" ;; + 12) region="sa-east-1" ;; esac ansible-playbook deploy.yml -t ec2,vpn -e "aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" From d4f8ea13ac3f4795a980e9052a1acc0182663459 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 21 Oct 2016 20:27:14 +0300 Subject: [PATCH 148/769] add prompts for optional features. resolved #103 --- algo | 89 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/algo b/algo index 6921beff..cb98f294 100755 --- a/algo +++ b/algo @@ -2,11 +2,50 @@ set -e +additional_roles () { +read -p " +Do you want to apply security enhancements? +[y/N]: " -r security_enabled +security_enabled=${security_enabled:-n} +if [[ "$security_enabled" == 'y' ]]; then ROLES+=" security"; fi + +read -p " +Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? +[y/N]: " -r proxy_enabled +proxy_enabled=${proxy_enabled:-n} +if [[ "$proxy_enabled" == 'y' ]]; then ROLES+=" proxy"; fi + +read -p " +Do you want to install a local DNS resolver to block ads while surfing? +[y/N]: " -r dns_enabled +dns_enabled=${dns_enabled:-n} +if [[ "$dns_enabled" == 'y' ]]; then ROLES+=" dns"; fi + +read -p " +Do you want to use auditd for security monitoring (see config.cfg)? +[y/N]: " -r logging_enabled +logging_enabled=${logging_enabled:-n} +if [[ "$logging_enabled" == 'y' ]]; then ROLES+=" logging"; fi + +read -p " +Do you want each user to have their own account for SSH tunneling? +[y/N]: " -r ssh_tunneling_enabled +ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} +if [[ "$ssh_tunneling_enabled" == 'y' ]]; then ROLES+=" ssh_tunneling"; fi + +} + +deploy () { + + ansible-playbook deploy.yml -t "${ROLES// /,}" -e "${EXTRA_VARS}" + +} + digitalocean () { read -p " Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): : " -rs do_access_token - + read -p " Enter an existing SSH key name (https://cloud.digitalocean.com/settings/security): : " -r do_ssh_name @@ -30,10 +69,10 @@ Name the vpn server: 10. Singapore 11. Toronto 12. Bangalore -Enter the number of your desired region: +Enter the number of your desired region: [7]: " -r region region=${region:-7} - + case "$region" in 1) do_region="ams2" ;; 2) do_region="ams3" ;; @@ -48,9 +87,9 @@ Enter the number of your desired region: 11) do_region="tor1" ;; 12) do_region="blr1" ;; esac - -ansible-playbook deploy.yml -t digitalocean,vpn -e "do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" +ROLES="digitalocean vpn" +EXTRA_VARS="do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" } ec2 () { @@ -63,7 +102,7 @@ Note: Make sure to use either your root key (recommended) or an IAM user with an Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [ABCD...]: " -rs aws_secret_key - + read -e -p " Enter the local path to your SSH public key: : " -i "~/.ssh/id_rsa.pub" -r ssh_public_key @@ -87,13 +126,13 @@ Name the vpn server: 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) 12. sa-east-1 South America (São Paulo) -Enter the number of your desired region: +Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} - - case "$aws_region" in + + case "$aws_region" in 1) region="us-east-1" ;; - 2) region="us-east-2" ;; + 2) region="us-east-2" ;; 3) region="us-west-1" ;; 4) region="us-west-2" ;; 5) region="ap-south-1" ;; @@ -105,16 +144,16 @@ Enter the number of your desired region: 11) region="eu-west-1" ;; 12) region="sa-east-1" ;; esac - -ansible-playbook deploy.yml -t ec2,vpn -e "aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" + ROLES="ec2 vpn" + EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" } gce () { read -p " -Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): +Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): : " -r credentials_file - + read -e -p " Enter the local path to your SSH public key: : " -i "~/.ssh/id_rsa.pub" -r ssh_public_key @@ -141,9 +180,9 @@ Name the vpn server: 13. East Asia (Taiwan C) Please choose the number of your zone. Press enter for default (#8) zone. [8]: " -r region - region=${region:-8} - - case "$region" in + region=${region:-8} + + case "$region" in 1) zone="us-central1-a" ;; 2) zone="us-central1-b" ;; 3) zone="us-central1-c" ;; @@ -158,16 +197,16 @@ Please choose the number of your zone. Press enter for default (#8) zone. 12) zone="asia-east1-b" ;; 13) zone="asia-east1-c" ;; esac - -ansible-playbook deploy.yml -t gce,vpn -e "credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" + ROLES="gce vpn" + EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" } non_cloud () { read -p " Enter IP address of your server: (use localhost for local installation) : " -r server_ip - + read -p " What user should we use to login on the server? (ignore if you're deploying to localhost) [root]: " -r server_user @@ -176,8 +215,10 @@ What user should we use to login on the server? (ignore if you're deploying to l read -p " Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) : " -r IP_subject - - ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" + + ROLES="local vpn" + EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" + } algo_provisioning () { @@ -201,6 +242,8 @@ Enter the number of your desired provider *) exit 1 ;; esac + additional_roles + deploy } user_management () { @@ -210,4 +253,4 @@ user_management () { case "$1" in update-users) user_management ;; *) algo_provisioning ;; -esac +esac From acc4667e478a68d2c6d8aa8d8f2811e210de891e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 22 Oct 2016 19:42:53 +0400 Subject: [PATCH 149/769] Update README.md #104 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6d1e1ce9..4cc84732 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Certificates and configuration files that users will need are placed in the `con Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices and installing a profile will fully configure the VPN. +### Android Devices + +You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. It's very simple to configure the StrongSwan VPN Client, just make a new profile with the IP address of your VPN server and choose which certificate to use. + ### StrongSwan Clients (e.g., OpenWRT) Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. These may be useful if you plan to set up a point-to-point VPN with OpenWRT or other custom device. From 44bc3ead48b2cbfd78bc4440ee8cad932ecc6fe9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 24 Oct 2016 17:53:08 +0300 Subject: [PATCH 150/769] set AllowTcpForwarding to local --- roles/ssh_tunneling/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index ea4d086e..7083431d 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -6,7 +6,7 @@ marker: '# ANSIBLE_MANAGED_ssh_tunneling_role' block: | Match Group algo - AllowTcpForwarding remote + AllowTcpForwarding local AllowAgentForwarding no AllowStreamLocalForwarding no PermitTunnel no From 0571563741a15a47c14b6c4c0b4a4b0228960ae2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 24 Oct 2016 18:08:33 +0300 Subject: [PATCH 151/769] ignore swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9df513b3..e1c9fea7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.retry configs/* inventory_users +*.kate-swp From d50bd439888c4747c47cbac8dc885043d70e9530 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 24 Oct 2016 18:08:58 +0300 Subject: [PATCH 152/769] Fix SSH keys permissions --- roles/ssh_tunneling/tasks/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 7083431d..ff787161 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -57,5 +57,10 @@ fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" +- name: Change mode for SSH private keys + local_action: file path=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem mode=0600 + with_items: "{{ users }}" + become: false + - name: Fetch the known_hosts file fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}_known_hosts flat=yes From a8bbc81369cae81e9fed9a71acca7c1ec144baac Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 24 Oct 2016 18:09:16 +0300 Subject: [PATCH 153/769] Fix prompts --- algo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/algo b/algo index cb98f294..1727abf3 100755 --- a/algo +++ b/algo @@ -7,31 +7,31 @@ read -p " Do you want to apply security enhancements? [y/N]: " -r security_enabled security_enabled=${security_enabled:-n} -if [[ "$security_enabled" == 'y' ]]; then ROLES+=" security"; fi +if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi read -p " Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? [y/N]: " -r proxy_enabled proxy_enabled=${proxy_enabled:-n} -if [[ "$proxy_enabled" == 'y' ]]; then ROLES+=" proxy"; fi +if [[ "$proxy_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" proxy"; fi read -p " Do you want to install a local DNS resolver to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" == 'y' ]]; then ROLES+=" dns"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi read -p " Do you want to use auditd for security monitoring (see config.cfg)? [y/N]: " -r logging_enabled logging_enabled=${logging_enabled:-n} -if [[ "$logging_enabled" == 'y' ]]; then ROLES+=" logging"; fi +if [[ "$logging_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" logging"; fi read -p " Do you want each user to have their own account for SSH tunneling? [y/N]: " -r ssh_tunneling_enabled ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} -if [[ "$ssh_tunneling_enabled" == 'y' ]]; then ROLES+=" ssh_tunneling"; fi +if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi } From ec8b62e099ea427f5bfb87f1cb6b76ec6df11329 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 24 Oct 2016 19:16:34 +0400 Subject: [PATCH 154/769] Update README.md #105 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cc84732..d3372f7d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,10 @@ Depending on the platform, you may need one or multiple of the following files. If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key file will be in the `config` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited. This is done to ensure that users have the least access required to tunnel through the server. -Make sure to access the server using 'ssh -N' with these limited accounts. +Make sure to access the server using 'ssh -N' with these limited accounts. +In order to make a tunnel you have to run this command: +`ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` +Don't forget to change `ip` and `user`. And then you can configure your browsers to use 127.0.0.1:1080 as sock4/5 ## Adding or Removing Users From 289807ead41734a563d1d136a872046b32900321 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 25 Oct 2016 21:33:46 +0300 Subject: [PATCH 155/769] fix dependencies --- roles/proxy/meta/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/proxy/meta/main.yml b/roles/proxy/meta/main.yml index e985f927..ef71a470 100644 --- a/roles/proxy/meta/main.yml +++ b/roles/proxy/meta/main.yml @@ -2,3 +2,4 @@ dependencies: - { role: common, tags: common } + - { role: vpn, tags: vpn } From 76ea7f67aed879e0a917b1775c2c3ab6d2d44f25 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 26 Oct 2016 18:56:23 +0300 Subject: [PATCH 156/769] extra vars added to use local DNS #110 --- algo | 2 +- config.cfg | 4 ---- roles/vpn/templates/ipsec.conf.j2 | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/algo b/algo index 1727abf3..c24fe1e3 100755 --- a/algo +++ b/algo @@ -19,7 +19,7 @@ read -p " Do you want to install a local DNS resolver to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi read -p " Do you want to use auditd for security monitoring (see config.cfg)? diff --git a/config.cfg b/config.cfg index c9cedd81..6db3c7e8 100644 --- a/config.cfg +++ b/config.cfg @@ -27,10 +27,6 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" -# Enable this variable if you want to use a local DNS resolver to block ads while surfing. (True or False) -service_dns: True - -# If you don't want to use a local DNS resolver (option `service_dns`) you need to define DNS servers in this list. dns_servers: - 8.8.8.8 - 8.8.4.4 diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index fa29458d..2bd6ad10 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -17,7 +17,7 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if service_dns is defined and service_dns == "Y" %} +{% if local_dns is defined and local_dns == "Y" %} rightdns={{ local_service_ip }} {% else %} rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} From 6c66cb03c7a382295120caf002faaca85420d7fe Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 26 Oct 2016 19:10:49 +0300 Subject: [PATCH 157/769] Fix for SSH timeout and attempts #111 --- ansible.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible.cfg b/ansible.cfg index dc8f8cd4..1a3afab2 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -8,5 +8,5 @@ host_key_checking = False record_host_keys = False [ssh_connection] -ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 scp_if_ssh = True From d052cb8e772af160c8a13b934f7146d53d1a7876 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 28 Oct 2016 21:00:11 +0300 Subject: [PATCH 158/769] skip-tags added. Fixed #121 --- algo | 12 +++++++----- roles/common/tasks/main.yml | 8 ++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/algo b/algo index c24fe1e3..9acb841d 100755 --- a/algo +++ b/algo @@ -2,6 +2,8 @@ set -e +SKIP_TAGS="_null" + additional_roles () { read -p " Do you want to apply security enhancements? @@ -37,7 +39,7 @@ if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi deploy () { - ansible-playbook deploy.yml -t "${ROLES// /,}" -e "${EXTRA_VARS}" + ansible-playbook deploy.yml -t "${ROLES// /,}" -e "${EXTRA_VARS}" --skip-tags "${SKIP_TAGS// /,}" } @@ -88,7 +90,7 @@ Enter the number of your desired region: 12) do_region="blr1" ;; esac -ROLES="digitalocean vpn" +ROLES="digitalocean vpn cloud" EXTRA_VARS="do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" } @@ -145,7 +147,7 @@ Enter the number of your desired region: 12) region="sa-east-1" ;; esac - ROLES="ec2 vpn" + ROLES="ec2 vpn cloud" EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" } @@ -198,7 +200,7 @@ Please choose the number of your zone. Press enter for default (#8) zone. 13) zone="asia-east1-c" ;; esac - ROLES="gce vpn" + ROLES="gce vpn cloud" EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" } @@ -218,7 +220,7 @@ Enter the public IP address of your server: (IMPORTANT! This IP is used to verif ROLES="local vpn" EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" - + SKIP_TAGS+=" cloud" } algo_provisioning () { diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 44aa3452..4b6e2ee1 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -12,6 +12,8 @@ args: executable: /bin/bash register: reboot_required + tags: + - cloud - name: Reboot shell: sleep 2 && shutdown -r now "Ansible updates triggered" @@ -19,16 +21,22 @@ poll: 0 when: reboot_required is defined and reboot_required.stdout == 'required' ignore_errors: true + tags: + - cloud - name: Wait for shutdown local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped timeout=120 when: reboot_required is defined and reboot_required.stdout == 'required' become: false + tags: + - cloud - name: Wait until SSH becomes ready... local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=120 when: reboot_required is defined and reboot_required.stdout == 'required' become: false + tags: + - cloud - name: Disable MOTD on login and SSHD replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" From 7cb2197d16b2f8bcea21051afb9746c5b8b94989 Mon Sep 17 00:00:00 2001 From: Nima Fatemi Date: Fri, 28 Oct 2016 19:48:28 +0000 Subject: [PATCH 159/769] Avoid using + for email address using + in email add (eg email+auditd@domain.tld) would cause auditd fail to start see #117 --- config.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 6db3c7e8..26f14584 100644 --- a/config.cfg +++ b/config.cfg @@ -5,7 +5,8 @@ users: - dan - jack -# Add an email address to send logs if you're using auditd for monitoring, +# Add an email address to send logs if you're using auditd for monitoring. +# Avoid using '+' in your email address otherwise auditd will fail to start. auditd_action_mail_acct: email@example.com # Exported certificates will be protected by the password below: From 5383c714991819fef58742554fb2266e4d4fa180 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 3 Nov 2016 17:21:18 +0300 Subject: [PATCH 160/769] Fixed #108 --- roles/vpn/tasks/main.yml | 4 +++- roles/vpn/templates/mobileconfig.j2 | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index fbe4b94e..6fff583c 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -170,6 +170,9 @@ - name: Set facts for mobileconfigs set_fact: proxy_enabled: false + pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" + VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" + CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" - name: Build the mobileconfigs template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 @@ -224,4 +227,3 @@ - include: iptables.yml tags: iptables - diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 5714839f..762848ab 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -44,7 +44,7 @@ LocalIdentifier {{ item.0 }} PayloadCertificateUUID - 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + {{ pkcs12_PayloadCertificateUUID }} CertificateType ECDSA256 ServerCertificateIssuerCommonName @@ -66,11 +66,11 @@ PayloadDisplayName VPN PayloadIdentifier - com.apple.vpn.managed.D247A30B-6023-4C8E-B3E3-FF1910A65E53 + com.apple.vpn.managed.{{ VPN_PayloadIdentifier }} PayloadType com.apple.vpn.managed PayloadUUID - D247A30B-6023-4C8E-B3E3-FF1910A65E53 + {{ VPN_PayloadIdentifier }} PayloadVersion 1 Proxies @@ -111,11 +111,11 @@ PayloadDisplayName {{ item.0 }}.p12 PayloadIdentifier - com.apple.security.pkcs12.1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + com.apple.security.pkcs12.{{ pkcs12_PayloadCertificateUUID }} PayloadType com.apple.security.pkcs12 PayloadUUID - 1FB2907D-14D3-4BAB-A472-B304F4B7F7D9 + {{ pkcs12_PayloadCertificateUUID }} PayloadVersion 1 @@ -131,11 +131,11 @@ PayloadDisplayName {{ IP_subject_alt_name }} PayloadIdentifier - com.apple.security.root.32EA3AAA-D19E-43EF-B357-608218745A38 + com.apple.security.root.{{ CA_PayloadIdentifier }} PayloadType com.apple.security.root PayloadUUID - 32EA3AAA-D19E-43EF-B357-608218745A38 + {{ CA_PayloadIdentifier }} PayloadVersion 1 @@ -148,16 +148,16 @@ {% endif %} PayloadIdentifier {% if proxy_enabled is defined and proxy_enabled == true %} - donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BA + donut.local.{{ 600000 | random | to_uuid | upper }} {% else %} - donut.local.37CA79B1-FC6A-421F-960A-90F91FC983BE + donut.local.{{ 500000 | random | to_uuid | upper }} {% endif %} PayloadRemovalDisallowed PayloadType Configuration PayloadUUID - 743B04A8-5725-45A2-B1BB-836F8C16DB0A + {{ 400000 | random | to_uuid | upper }} PayloadVersion 1 From 29de003b2d47399c5f2f0a43bcccf02c339bd8b3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 3 Nov 2016 18:05:56 +0300 Subject: [PATCH 161/769] inplemented #109 --- algo | 20 +++++++++++++ roles/vpn/templates/mobileconfig.j2 | 44 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/algo b/algo index 9acb841d..4911274d 100755 --- a/algo +++ b/algo @@ -35,6 +35,26 @@ Do you want each user to have their own account for SSH tunneling? ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi +read -p " +Do you want to enable VPN always when connected to Wi-Fi? +[y/N]: " -r OnDemandEnabled_WIFI +OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} +if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi + +if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then + read -p " +Do you want to exclude trust Wi-Fi networks from VPN usage? (eg: Your home network. Comma-separated value, eg: HomeMeganet,OfficeSuperWifi,AlgoWiFi) +: " -r OnDemandEnabled_WIFI_ECXLUDE + OnDemandEnabled_WIFI_ECXLUDE=${OnDemandEnabled_WIFI_ECXLUDE:-_null} + EXTRA_VARS+=" OnDemandEnabled_WIFI_ECXLUDE=$OnDemandEnabled_WIFI_ECXLUDE" +fi + +read -p " +Do you want to enable VPN always when connected to the cellular network? +[y/N]: " -r OnDemandEnabled_Cellular +OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} +if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi + } deploy () { diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 762848ab..d7ac8998 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -6,7 +6,51 @@ IKEv2 +{% if (OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y') or (OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y') %} + OnDemandEnabled + 1 + OnDemandRules + +{% if OnDemandEnabled_WIFI_ECXLUDE is defined and OnDemandEnabled_WIFI_ECXLUDE != '_null' %} +{% set WIFI_ECXLUDE_LIST = OnDemandEnabled_WIFI_ECXLUDE.split(',') %} + + Action + Disconnect + InterfaceTypeMatch + WiFi + SSIDMatch + +{% for network_name in WIFI_ECXLUDE_LIST %} + {{ network_name }} +{% endfor %} + + +{% else %} +{% endif %} + + Action +{% if OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y' %} + Connect + {% else %} + Disconnect +{% endif %} + InterfaceTypeMatch + WiFi + + + Action +{% if OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y' %} + Connect + {% else %} + Disconnect +{% endif %} + InterfaceTypeMatch + Cellular + + +{% else %} +{% endif %} AuthenticationMethod Certificate ChildSecurityAssociationParameters From 09bbc4058c287db52b65935c27bf8dfecc129b57 Mon Sep 17 00:00:00 2001 From: Kevin Cernekee Date: Sun, 6 Nov 2016 09:40:07 -0800 Subject: [PATCH 162/769] Add missing tags in common playbook If the common playbook is invoked with the "cloud" tag, non-cloud tasks will be skipped. On GCE this causes "Install tools" to be skipped, apparmor-utils is not installed, and then the "Enforcing ipsec with apparmor" step fails. --- roles/common/tasks/main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 4b6e2ee1..9cdb88dc 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -2,9 +2,13 @@ - name: Gather Facts setup: + tags: + - always - name: Install software updates apt: update_cache=yes upgrade=dist + tags: + - cloud - name: Check if reboot is required shell: > @@ -43,6 +47,8 @@ with_items: - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } + tags: + - cloud - name: Install tools apt: name="{{ item }}" state=latest @@ -55,24 +61,36 @@ - sendmail - iptables-persistent - cgroup-tools + tags: + - always - name: Loopback for services configured template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg notify: - restart loopback + tags: + - always - name: Loopback included into the network config lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present notify: - restart loopback + tags: + - always - meta: flush_handlers + tags: + - always - name: Enable packet forwarding for IPv4 sysctl: name="{{ item }}" value=1 with_items: - net.ipv4.ip_forward - net.ipv4.conf.all.forwarding + tags: + - always - name: Enable packet forwarding for IPv6 sysctl: name=net.ipv6.conf.all.forwarding value=1 + tags: + - always From 433389c0aba2dde05470e589e84c90bd8d59ae87 Mon Sep 17 00:00:00 2001 From: Kevin Cernekee Date: Sun, 6 Nov 2016 09:42:58 -0800 Subject: [PATCH 163/769] Use /var/run/reboot-required to determine if a restart is needed The current check only looks to see if a new kernel was installed. --- roles/common/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 9cdb88dc..a5730ac1 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -12,7 +12,7 @@ - name: Check if reboot is required shell: > - if [[ $(readlink -f /vmlinuz) != /boot/vmlinuz-$(uname -r) ]]; then echo "required"; else echo "no"; fi + if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi args: executable: /bin/bash register: reboot_required From 3e3d7c6fa78b45d5525f3d45ab722af30971d902 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 23 Nov 2016 20:28:05 +0300 Subject: [PATCH 164/769] Update readme. Fix #120 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d3372f7d..2bd41a89 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw The easiest way to get an Algo server running is to let it setup a new virtual machine in the cloud for you. -1. Install the dependencies on OS X or Linux: `sudo easy_install pip && sudo pip install -r requirements.txt` +1. Install the dependencies +1.1. On OS X: `sudo easy_install pip && sudo pip install -r requirements.txt` +1.2. On Linux (deb based): `sudo easy_install pip && sudo apt-get install libssl-dev && sudo pip install -r requirements.txt` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 3. Start the deploy and follow the instructions: `./algo` From 047f68df2f248195583a840f6a7ebc2192fc85ba Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 23 Nov 2016 20:34:53 +0300 Subject: [PATCH 165/769] Change the site in the congrats handler to whoer.net in order to clarify the message at the end of the install about testing VPN. Fix #110 --- roles/vpn/handlers/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 84e08b04..26ba6fff 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -20,7 +20,7 @@ - "# Congratulations! #" - "# Your Algo server is running. #" - "# Config files and certificates are in the ./configs/ directory. #" - - "# Go to https://www.dnsleaktest.com/ after connecting #" + - "# Go to https://whoer.net/ after connecting #" - "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" - "#----------------------------------------------------------------------#" From 1dc6e1a0fa6d9fa5d47c58043bf50706af8ce465 Mon Sep 17 00:00:00 2001 From: Defunct Date: Thu, 27 Oct 2016 19:00:43 +0000 Subject: [PATCH 166/769] resolves #118 - AWS env keys --- roles/cloud-ec2/tasks/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index eace8c4d..e2b0a65c 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,7 +1,7 @@ - name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" owner: 099720109477 sort: name @@ -15,8 +15,8 @@ - name: Add ssh public key ec2_key: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" name: VPNKEY region: "{{ region }}" key_material: "{{ item }}" @@ -25,8 +25,8 @@ - name: Configure EC2 security group ec2_group: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" name: vpn-secgroup description: Security group for VPN servers region: "{{ region }}" @@ -51,8 +51,8 @@ - name: Launch instance ec2: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" keypair: "VPNKEY" group: vpn-secgroup instance_type: t2.nano From 74b9f0a15a2d466726960eeabc6a840ad6bc5048 Mon Sep 17 00:00:00 2001 From: Defunct Date: Thu, 27 Oct 2016 19:29:19 +0000 Subject: [PATCH 167/769] support older bash versions - resolves #116 --- algo | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/algo b/algo index c24fe1e3..eba3eb42 100755 --- a/algo +++ b/algo @@ -103,9 +103,10 @@ Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [ABCD...]: " -rs aws_secret_key - read -e -p " -Enter the local path to your SSH public key: -: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key + + read -p " +Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key_file +ssh_public_key=${ssh_public_key_file:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: @@ -154,9 +155,9 @@ gce () { Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): : " -r credentials_file - read -e -p " -Enter the local path to your SSH public key: -: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key + read -p " +Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key +ssh_public_key=${ssh_public_key_file:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: From 437d659eb638545a26395c75f374ab61cc8c95fb Mon Sep 17 00:00:00 2001 From: Defunct Date: Sun, 13 Nov 2016 18:44:41 +0000 Subject: [PATCH 168/769] resolves #126 - incorrect private key usage w/o ssh-agent --- algo | 8 ++++---- roles/cloud-ec2/tasks/main.yml | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/algo b/algo index eba3eb42..8eeb05af 100755 --- a/algo +++ b/algo @@ -105,10 +105,10 @@ Note: Make sure to use either your root key (recommended) or an IAM user with an read -p " -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key_file -ssh_public_key=${ssh_public_key_file:-$HOME/.ssh/id_rsa.pub} +Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key + ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} - read -p " +read -p " Name the vpn server: [algo]: " -r aws_server_name aws_server_name=${aws_server_name:-algo} @@ -157,7 +157,7 @@ Enter the local path to your credentials JSON file (https://support.google.com/c read -p " Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key -ssh_public_key=${ssh_public_key_file:-$HOME/.ssh/id_rsa.pub} + ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index e2b0a65c..4b8de61e 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -66,6 +66,7 @@ - name: Add new instance to host group add_host: hostname: "{{ item.public_ip }}" + ansible_ssh_private_key_file: "{{ ssh_public_key[:-4] }}" groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" From 27ea98e7a88d665e4f4b13c8f1cee2c37cfcd557 Mon Sep 17 00:00:00 2001 From: fkt Date: Sat, 26 Nov 2016 18:05:06 +0000 Subject: [PATCH 169/769] Show congrats message at the end - #115 --- deploy.yml | 15 +++++++++++++++ roles/vpn/handlers/main.yml | 12 ------------ roles/vpn/tasks/main.yml | 2 -- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/deploy.yml b/deploy.yml index 9fbf792f..7fecd165 100644 --- a/deploy.yml +++ b/deploy.yml @@ -38,3 +38,18 @@ handlers: - name: reload eth0 shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' + + post_tasks: + - shell: | + echo "#----------------------------------------------------------------------#" + echo "# Congratulations! #" + echo "# Your Algo server is running. #" + echo "# Config files and certificates are in the ./configs/ directory. #" + echo "# Go to https://whoer.net/ after connecting #" + echo "# and ensure that all your traffic passes through the VPN. #" + echo "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" + echo "#----------------------------------------------------------------------#" + tags: always + register: congrats + - debug: msg="{{ congrats.stdout_lines }}" + tags: always diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 26ba6fff..32885b5f 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -12,15 +12,3 @@ - name: restart iptables service: name=netfilter-persistent state=restarted - -- name: congrats - debug: - msg: - - "#----------------------------------------------------------------------#" - - "# Congratulations! #" - - "# Your Algo server is running. #" - - "# Config files and certificates are in the ./configs/ directory. #" - - "# Go to https://whoer.net/ after connecting #" - - "# and ensure that all your traffic passes through the VPN. #" - - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" - - "#----------------------------------------------------------------------#" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 6fff583c..53734b76 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -222,8 +222,6 @@ - name: Fetch server CA certificate fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes - notify: - - congrats - include: iptables.yml tags: iptables From ee95846445a2d0989257cf0b70c3a842573444bb Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 26 Nov 2016 23:22:12 +0300 Subject: [PATCH 170/769] mobileconfig fix --- roles/vpn/templates/mobileconfig.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index d7ac8998..e7966216 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -6,8 +6,8 @@ IKEv2 -{% if (OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y') or (OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y') %} +{% if (OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y') or (OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y') %} OnDemandEnabled 1 OnDemandRules From d708750bd1df2642d353cd3c4a2a3295ed80f4c9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 26 Nov 2016 23:42:04 +0300 Subject: [PATCH 171/769] Issue template --- .github/ISSUE_TEMPLATE.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..0689e365 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ +### OS / Environment + + + +### Ansible version + + + +### Version of components from `requirements.txt` + + + +### Summary of the problem + + + +### Steps to reproduce the behavior + + + +### Expected behavior + + + +### Actual behavior + + From 2cb98b4516038bce9c455f149d164f905c61092a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 27 Nov 2016 01:37:17 +0300 Subject: [PATCH 172/769] Windows RSA support #9 --- algo | 6 ++++++ config.cfg | 2 -- roles/vpn/templates/client_ipsec.conf.j2 | 8 ++++++++ roles/vpn/templates/client_ipsec.secrets.j2 | 5 ++++- roles/vpn/templates/easy-rsa.vars.j2 | 4 ++++ roles/vpn/templates/ipsec.conf.j2 | 8 ++++++++ roles/vpn/templates/ipsec.secrets.j2 | 5 ++++- 7 files changed, 34 insertions(+), 4 deletions(-) diff --git a/algo b/algo index 4911274d..cd224d84 100755 --- a/algo +++ b/algo @@ -55,6 +55,12 @@ Do you want to enable VPN always when connected to the cellular network? OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi +read -p " +Do you want to enable VPN for Windows 10 clients? (Will use insecure algorithms and ciphers) +[y/N]: " -r Win10_Enabled +Win10_Enabled=${Win10_Enabled:-n} +if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi + } deploy () { diff --git a/config.cfg b/config.cfg index 26f14584..51f00219 100644 --- a/config.cfg +++ b/config.cfg @@ -59,8 +59,6 @@ ipsec_config: dpddelay: '35s' rekey: 'no' keyexchange: 'ikev2' - ike: 'aes128gcm16-sha2_256-prfsha256-ecp256!' - esp: 'aes128gcm16-sha2_256-ecp256!' compress: 'yes' fragmentation: 'yes' diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 3b01ff16..2e97c36b 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -3,6 +3,14 @@ conn ikev2-{{ IP_subject_alt_name }} {{ key }}={{ value }} {% endfor %} +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} + ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! + esp=aes128gcm16-sha2_256-ecp256,aes256-sha1-modp1024! +{% else %} + ike=aes128gcm16-sha2_256-prfsha256-ecp256 + esp=aes128gcm16-sha2_256-ecp256 +{% endif %} + right={{ IP_subject_alt_name }} rightid={{ IP_subject_alt_name }} rightsubnet=0.0.0.0/0 diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/vpn/templates/client_ipsec.secrets.j2 index ec4a30fa..61603129 100644 --- a/roles/vpn/templates/client_ipsec.secrets.j2 +++ b/roles/vpn/templates/client_ipsec.secrets.j2 @@ -1,2 +1,5 @@ +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +{{ IP_subject_alt_name }} : RSA {{ IP_subject_alt_name }}_{{ item }}.key +{% else %} {{ IP_subject_alt_name }} : ECDSA {{ IP_subject_alt_name }}_{{ item }}.key - +{% endif %} diff --git a/roles/vpn/templates/easy-rsa.vars.j2 b/roles/vpn/templates/easy-rsa.vars.j2 index 50159aa6..2805b3b6 100644 --- a/roles/vpn/templates/easy-rsa.vars.j2 +++ b/roles/vpn/templates/easy-rsa.vars.j2 @@ -102,7 +102,11 @@ set_var EASYRSA_DN "cn_only" # * rsa # * ec +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +set_var EASYRSA_ALGO rsa +{% else %} set_var EASYRSA_ALGO ec +{% endif %} # Define the named curve, used in ec mode only: diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 2bd6ad10..c412994d 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -7,6 +7,14 @@ conn %default {{ key }}={{ value }} {% endfor %} +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} + ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! + esp=aes128gcm16-sha2_256-ecp256,aes256-sha1-modp1024! +{% else %} + ike=aes128gcm16-sha2_256-prfsha256-ecp256 + esp=aes128gcm16-sha2_256-ecp256 +{% endif %} + left=%any leftauth=pubkey leftid={{ IP_subject_alt_name }} diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/vpn/templates/ipsec.secrets.j2 index d5793aea..2226f04e 100644 --- a/roles/vpn/templates/ipsec.secrets.j2 +++ b/roles/vpn/templates/ipsec.secrets.j2 @@ -1,2 +1,5 @@ +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +: RSA {{ IP_subject_alt_name }}.key +{% else %} : ECDSA {{ IP_subject_alt_name }}.key - +{% endif %} From e90b58802d5ab74c36426d29975a3b5ccb58237d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 27 Nov 2016 12:44:05 +0300 Subject: [PATCH 173/769] fix in the mobileconfig template --- roles/vpn/templates/mobileconfig.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index e7966216..1ccb0374 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -90,7 +90,11 @@ PayloadCertificateUUID {{ pkcs12_PayloadCertificateUUID }} CertificateType +{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} + RSA2048 +{% else %} ECDSA256 +{% endif %} ServerCertificateIssuerCommonName {{ IP_subject_alt_name }} RemoteAddress From e40545cce5ec960ea97bb9da397af3a858882a11 Mon Sep 17 00:00:00 2001 From: defunct Date: Sun, 27 Nov 2016 12:55:05 -0500 Subject: [PATCH 174/769] opens #126 This commit reverts changes in 437d659 to avoid breaking changes. --- roles/cloud-ec2/tasks/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 4b8de61e..e2b0a65c 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -66,7 +66,6 @@ - name: Add new instance to host group add_host: hostname: "{{ item.public_ip }}" - ansible_ssh_private_key_file: "{{ ssh_public_key[:-4] }}" groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" From ad162f55a2d355af473396b4e05e6b1a96476864 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 29 Nov 2016 18:46:58 +0300 Subject: [PATCH 175/769] here were no credentials #127 --- roles/cloud-ec2/tasks/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 3373614e..1dc0eda2 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -25,6 +25,8 @@ - name: Configure EC2 virtual private clouds ec2_vpc: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" state: present resource_tags: { "Environment":"Algo" } region: "{{ region }}" From f6166ccde4f8c878f07af69099167026b29a0731 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 29 Nov 2016 22:14:18 +0300 Subject: [PATCH 176/769] modify ciphers #9 --- roles/vpn/templates/ipsec.conf.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index c412994d..58089c10 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -9,10 +9,10 @@ conn %default {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! - esp=aes128gcm16-sha2_256-ecp256,aes256-sha1-modp1024! + esp=aes128gcm16-sha2_256-ecp256,aes256-sha2_256-modp2048! {% else %} - ike=aes128gcm16-sha2_256-prfsha256-ecp256 - esp=aes128gcm16-sha2_256-ecp256 + ike=aes128gcm16-sha2_256-prfsha256-ecp256! + esp=aes128gcm16-sha2_256-ecp256! {% endif %} left=%any From 8a0c5ab9718dafeb169239c8a4e47e5fce180f9e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 29 Nov 2016 23:00:01 +0300 Subject: [PATCH 177/769] Windows support implemented --- README.md | 8 ++++++++ roles/vpn/tasks/main.yml | 12 ++++++++++++ roles/vpn/templates/client_windows.ps1.j2 | 3 +++ 3 files changed, 23 insertions(+) create mode 100644 roles/vpn/templates/client_windows.ps1.j2 diff --git a/README.md b/README.md index 2bd41a89..e9e4bc9a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do Certificates and configuration files that users will need are placed in the `config` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. +### Windows Devices + +You have to import the corresponding client certificate to The Personal store and the corresponding CA certificate to The Local Machine Trusted Root store.
+Add an IKEv2 connection in the network settings and then, activate additional ciphers via powershell:
+`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA25612 +8 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` (change Algo on the vpn connection name)
+Also, you can find the powershell script and the p12 certificate in the configs directory and run it as Administrator on your machine. + ### Apple Devices Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices and installing a profile will fully configure the VPN. diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 53734b76..dacc7368 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -191,6 +191,8 @@ with_items: - "{{ users }}" + + - name: Fetch users P12 fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" @@ -215,6 +217,16 @@ fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.secrets dest=configs/{{ IP_subject_alt_name }}_{{ item }}_ipsec.secrets flat=yes with_items: "{{ users }}" +- name: Build the windows client powershell script + template: src=client_windows.ps1.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/windows_{{ item }}.ps1 mode=0600 + when: Win10_Enabled is defined and Win10_Enabled == "Y" + with_items: "{{ users }}" + +- name: Fetch users windows scripts + fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/windows_{{ item }}.ps1 dest=configs/{{ IP_subject_alt_name }}_{{ item }}_windows.ps1 flat=yes + when: Win10_Enabled is defined and Win10_Enabled == "Y" + with_items: "{{ users }}" + - name: Restrict permissions file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root with_items: diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 new file mode 100644 index 00000000..9b6d1970 --- /dev/null +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -0,0 +1,3 @@ +certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ IP_subject_alt_name }}_{{ item }}.p12 +Add-VpnConnection -name "Algo" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none From 3d53dde6ca43cf05b80745a0686bb8d1e14f09d5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 6 Dec 2016 20:14:08 +0300 Subject: [PATCH 178/769] Fixed. #137 --- config.cfg | 4 ++++ roles/vpn/tasks/main.yml | 5 ----- users.yml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config.cfg b/config.cfg index 51f00219..34a1908e 100644 --- a/config.cfg +++ b/config.cfg @@ -64,3 +64,7 @@ ipsec_config: # IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 + +pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" +VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" +CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index dacc7368..f5951a45 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -170,9 +170,6 @@ - name: Set facts for mobileconfigs set_fact: proxy_enabled: false - pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" - VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" - CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" - name: Build the mobileconfigs template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 @@ -191,8 +188,6 @@ with_items: - "{{ users }}" - - - name: Fetch users P12 fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes with_items: "{{ users }}" diff --git a/users.yml b/users.yml index 6bdbf2e4..fb79ba20 100644 --- a/users.yml +++ b/users.yml @@ -139,7 +139,7 @@ remove: yes force: yes when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ valid_users.stdout_lines }}" + with_items: "{{ valid_users.stdout_lines | default('null') }}" - name: SSH | Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes From b71a09ef07e7fb12fb93ac2797303b11ec9233ae Mon Sep 17 00:00:00 2001 From: Defunct Date: Fri, 9 Dec 2016 16:06:19 +0000 Subject: [PATCH 179/769] EC2 Canada - resolves #141 --- algo | 2 ++ 1 file changed, 2 insertions(+) diff --git a/algo b/algo index 8f1db943..75eda1ae 100755 --- a/algo +++ b/algo @@ -155,6 +155,7 @@ Name the vpn server: 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) 12. sa-east-1 South America (São Paulo) + 13. ca-central-1 Canada (Central) Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} @@ -172,6 +173,7 @@ Enter the number of your desired region: 10) region="eu-central-1" ;; 11) region="eu-west-1" ;; 12) region="sa-east-1" ;; + 13) region="ca-central-1" ;; esac ROLES="ec2 vpn cloud" From 27e5a4fecaab4c2d918b9e45b0a3547386183755 Mon Sep 17 00:00:00 2001 From: Defunct Date: Fri, 9 Dec 2016 20:45:12 +0000 Subject: [PATCH 180/769] Sort by latest AMI - resolves #140 --- roles/cloud-ec2/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index e2b0a65c..ae4fbab7 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -4,7 +4,7 @@ aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" owner: 099720109477 - sort: name + sort: creationDate sort_order: descending sort_end: 1 region: "{{ region }}" From 83a93f19ece8fb3d2655336e9fb41393760bc960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20Gy=C3=B6rffy?= Date: Sat, 10 Dec 2016 15:53:34 +0200 Subject: [PATCH 181/769] Fix configs path in the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e9e4bc9a..f04b853a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do ## Configure the VPN Clients -Certificates and configuration files that users will need are placed in the `config` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. +Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. ### Windows Devices @@ -75,7 +75,7 @@ Depending on the platform, you may need one or multiple of the following files. ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key file will be in the `config` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited. This is done to ensure that users have the least access required to tunnel through the server. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key file will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited. This is done to ensure that users have the least access required to tunnel through the server. Make sure to access the server using 'ssh -N' with these limited accounts. In order to make a tunnel you have to run this command: From c5526027249777b57a0565b90c1f9398c8fc1759 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 10 Dec 2016 21:09:34 +0300 Subject: [PATCH 182/769] Azure support #26 --- algo | 81 ++++++++++++++++++++++++++- azure.yml | 96 -------------------------------- deploy.yml | 3 +- requirements.txt | 2 +- roles/cloud-azure/tasks/main.yml | 70 +++++++++++++++++++++++ 5 files changed, 152 insertions(+), 100 deletions(-) delete mode 100644 azure.yml diff --git a/algo b/algo index 8f1db943..667642d7 100755 --- a/algo +++ b/algo @@ -69,6 +69,81 @@ deploy () { } +azure () { + read -p " +Enter your azure secret (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +[...]: " -rs azure_secret + + read -p " + +Enter your azure tenant (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +[...]: " -rs azure_tenant + + read -p " + +Enter your azure client_id (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +[...]: " -rs azure_client_id + + read -p " + +Enter your azure subscription_id (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +[...]: " -rs azure_subscription_id + + read -e -p " + +Enter the local path to your SSH public key: +: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key + + read -p " +Name the vpn server: +[algo]: " -r azure_server_name + azure_server_name=${azure_server_name:-algo} + + read -p " + What region should the server be located in? + 1. South Central US + 2. Central US + 3. North Europe + 4. West Europe + 5. Southeast Asia + 6. Japan West + 7. Japan East + 8. Australia Southeast + 9. Australia East + 10. Canada Central + 11. West US 2 + 12. West Central US + 13. UK South + 14. UK West +Enter the number of your desired region: +[1]: " -r azure_region + azure_region=${azure_region:-1} + + case "$azure_region" in + 1) region="southcentralus" ;; + 2) region="centralus" ;; + 3) region="northeurope" ;; + 4) region="westeurope" ;; + 5) region="southeastasia" ;; + 6) region="japanwest" ;; + 7) region="japaneast" ;; + 8) region="australiasoutheast" ;; + 9) region="australiaeast" ;; + 10) region="canadacentral" ;; + 11) region="westus2" ;; + 12) region="westcentralus" ;; + 13) region="uksouth" ;; + 14) region="ukwest" ;; + esac + + ROLES="azure vpn cloud" + EXTRA_VARS="azure_secret=$azure_secret azure_tenant=$azure_tenant azure_client_id=$azure_client_id azure_subscription_id=$azure_subscription_id azure_server_name=$azure_server_name ssh_public_key=$ssh_public_key region=$region" +} + digitalocean () { read -p " Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): @@ -256,7 +331,8 @@ algo_provisioning () { 1. DigitalOcean 2. Amazon EC2 3. Google Compute Engine - 4. Install to existing Ubuntu server + 4. Microsoft Azure + 5. Install to existing Ubuntu server Enter the number of your desired provider : " @@ -267,7 +343,8 @@ Enter the number of your desired provider 1) digitalocean; ;; 2) ec2; ;; 3) gce; ;; - 4) non_cloud; ;; + 4) azure; ;; + 5) non_cloud; ;; *) exit 1 ;; esac diff --git a/azure.yml b/azure.yml deleted file mode 100644 index ec15d2cc..00000000 --- a/azure.yml +++ /dev/null @@ -1,96 +0,0 @@ -- name: Configure the server and install required software - hosts: localhost - gather_facts: false - - vars: - regions: - "1": "East US" - "2": "West US" - "3": "South Central US" - "4": "North Europe" - "5": "East Asia" - "6": "Japan East" - "7": "West Europe" - "8": "Southeast Asia" - "9": "Japan West" - "10": "North Central US" - "11": "Central US" - "12": "Brazil South" - "13": "East US 2" - "14": "Australia Southeast" - "15": "Australia East" - - #vars_prompt: - #- name: "azure_subscription_id" - #prompt: "Enter your subscription ID (https://blogs.msdn.microsoft.com/mschray/2015/05/13/getting-your-azure-guid-subscription-id/):\n" - #private: yes - - #- name: "management_cert_path" - #prompt: "Enter the local path to your management cert [ex: ~/.ssh/id_rsa.pub] (https://azure.microsoft.com/en-us/documentation/articles/azure-api-management-certs/):\n" - #private: no - - #- name: "ssh_public_key" - #prompt: "Enter the local path to your SSH public key [ex: ~/.ssh/id_rsa.pub] :\n" - #private: no - - #- name: "region" - #prompt: > - #What region should the server be located in? - #1. East US - #2. West US - #3. South Central US - #4. North Europe - #5. East Asia - #6. Japan East - #7. West Europe - #8. Southeast Asia - #9. Japan West - #10. North Central US - #11. Central US - #12. Brazil South - #13. East US 2 - #14. Australia Southeast - #15. Australia East - #Enter the number of your desired region: - #default: "7" - #private: no - - #- name: "azure_server_name" - #prompt: "Name the vpn server:\n" - #default: "algo.local" - #private: no - - #- name: "dns_enabled" - #prompt: "Do you want to use a local DNS resolver to block ads while surfing? (Y or N):\n" - #default: "Y" - #private: no - - #- name: "auditd_enabled" - #prompt: "Do you want to use auditd ? (Y or N):\n" - #default: "Y" - #private: no - - roles: - - cloud-azure - -- name: Post-provisioning tasks - hosts: vpn-host - gather_facts: false - become: true - vars_files: - - config.cfg - - pre_tasks: - - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - - name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - - roles: - - common - - security - - proxy - - vpn - - { role: dns_adblocking , when: dns_enabled is defined and dns_enabled == "Y" } - - { role: logging, when: auditd_enabled is defined and auditd_enabled == 'Y' } - diff --git a/deploy.yml b/deploy.yml index 7fecd165..b6c8380a 100644 --- a/deploy.yml +++ b/deploy.yml @@ -8,6 +8,7 @@ - { role: cloud-digitalocean, tags: ['digitalocean'] } - { role: cloud-ec2, tags: ['ec2'] } - { role: cloud-gce, tags: ['gce'] } + - { role: cloud-azure, tags: ['azure'] } - { role: local, tags: ['local'] } - name: Post-provisioning tasks @@ -21,7 +22,7 @@ pre_tasks: - name: Common pre-tasks include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'pre' ] + tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'pre' ] - name: DigitalOcean pre-tasks include: playbooks/digitalocean.yml diff --git a/requirements.txt b/requirements.txt index 36b226c9..3039915a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ ansible>=2.1 dopy==0.3.5 boto -azure>=0.7.1 +azure==2.0.0rc5 apache-libcloud six diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 8b137891..d894b2e5 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1 +1,71 @@ +--- +- set_fact: + resource_group: "Algo_{{ region }}" + +- name: Create a resource group + azure_rm_resourcegroup: + secret: "{{ azure_secret | default(lookup('env','AZURE_CLIENT_ID')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_SECRET')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_TENANT')) }}" + name: "{{ resource_group }}" + location: "{{ region }}" + tags: + service: algo + +- name: Create a virtual network + azure_rm_virtualnetwork: + resource_group: "{{ resource_group }}" + name: algo_net + address_prefixes: "10.10.0.0/16" + tags: + service: algo + +- name: Create a subnet + azure_rm_subnet: + resource_group: "{{ resource_group }}" + name: algo_subnet + address_prefix: "10.10.0.0/24" + virtual_network: algo_net + tags: + service: algo + +- name: Create an instance + azure_rm_virtualmachine: + secret: "{{ azure_secret | default(lookup('env','AZURE_CLIENT_ID')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_SECRET')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_TENANT')) }}" + resource_group: "{{ resource_group }}" + admin_username: ubuntu + virtual_network: algo_net + name: "{{ azure_server_name }}" + ssh_password_enabled: false + vm_size: Standard_D1 + tags: + service: algo + ssh_public_keys: + - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ ssh_public_key }}') }}" } + image: + offer: UbuntuServer + publisher: Canonical + sku: '16.04-LTS' + version: latest + register: azure_rm_virtualmachine + +- set_fact: + ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" + +- name: Add the instance to an inventory group + add_host: + name: "{{ ip_address }}" + groups: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" + cloud_provider: azure + ipv6_support: no + +- name: Wait for SSH to become available + local_action: "wait_for port=22 host={{ ip_address }} timeout=320" From 4b50cd70c09a10ab87d77f1ef406f9fbae59c14b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 12 Dec 2016 02:41:45 -0500 Subject: [PATCH 183/769] Update README.md --- README.md | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index f04b853a..b5965673 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw ## Features -* Supports only IKEv2 w/ a single cipher suite: AES GCM, SHA2 HMAC, and P-256 DH +* Supports only IKEv2 w/ a single cipher suite: AES-GCM, HMAC-SHA2, and P-256 DH * Generates Apple Profiles to auto-configure iOS and macOS devices * Provides helper scripts to add and remove users * Blocks ads with a local DNS resolver and HTTP proxy (optional) -* Sets up limited SSH tunnels for each user (optional) -* Based on current versions of Ubuntu and StrongSwan -* Installs to DigitalOcean, Amazon EC2, Google Cloud Engine, or your own server +* Sets up limited SSH users for tunneling traffic (optional) +* Based on current versions of Ubuntu and strongSwan +* Installs to DigitalOcean, Amazon EC2, Google Cloud Engine, Microsoft Azure or your own server ## Anti-features @@ -25,11 +25,13 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw ## Deploy the Algo Server -The easiest way to get an Algo server running is to let it setup a new virtual machine in the cloud for you. +The easiest way to get an Algo server running is to let it setup a _new_ virtual machine in the cloud for you. + +1. Install the dependencies for your operating system: + + OS X: `sudo easy_install pip && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo easy_install pip && sudo apt-get install libssl-dev && sudo pip install -r requirements.txt` -1. Install the dependencies -1.1. On OS X: `sudo easy_install pip && sudo pip install -r requirements.txt` -1.2. On Linux (deb based): `sudo easy_install pip && sudo apt-get install libssl-dev && sudo pip install -r requirements.txt` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 3. Start the deploy and follow the instructions: `./algo` @@ -41,14 +43,6 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. -### Windows Devices - -You have to import the corresponding client certificate to The Personal store and the corresponding CA certificate to The Local Machine Trusted Root store.
-Add an IKEv2 connection in the network settings and then, activate additional ciphers via powershell:
-`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA25612 -8 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` (change Algo on the vpn connection name)
-Also, you can find the powershell script and the p12 certificate in the configs directory and run it as Administrator on your machine. - ### Apple Devices Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices and installing a profile will fully configure the VPN. @@ -57,6 +51,15 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. It's very simple to configure the StrongSwan VPN Client, just make a new profile with the IP address of your VPN server and choose which certificate to use. +### Windows + +Import your user certificate to your Personal certificate store and your CA certificate to the Local Machine Trusted Root certificate store. Then, add an IKEv2 connection in the network settings and activate additional ciphers for it via Powershell (change the ConnectionName to the name of your IKEv2 connection): + +`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA25612 +8 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` + +Note that an all-in-one Powershell script that imports your personal certificate, sets up the VPN connection, and activates the stronger ciphers for it is included in the `configs` folder. + ### StrongSwan Clients (e.g., OpenWRT) Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. These may be useful if you plan to set up a point-to-point VPN with OpenWRT or other custom device. @@ -72,15 +75,15 @@ Depending on the platform, you may need one or multiple of the following files. * user.key: User Private Key * user.mobileconfig: Apple Profile * user.p12: User Certificate and Private Key (in PKCS#12 format) +* user_windows.ps1: Powershell script to setup a VPN connection on Windows ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key file will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited. This is done to ensure that users have the least access required to tunnel through the server. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited (`ssh -N` is required). This is done to ensure that users have the least access required to tunnel through the server. -Make sure to access the server using 'ssh -N' with these limited accounts. -In order to make a tunnel you have to run this command: -`ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` -Don't forget to change `ip` and `user`. And then you can configure your browsers to use 127.0.0.1:1080 as sock4/5 +Use the command below to start an SSH tunnel, replacing `ip` and `user` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through Algo. + + `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` ## Adding or Removing Users @@ -89,7 +92,7 @@ Algo's own scripts can easily add and remove users from the VPN server. 1. Update the `users` list in your `config.cfg` 2. Run the command: `./algo update-users` -The Algo VPN server now only contains the users listed in the `config.cfg` file. +The Algo VPN server now contains only the users listed in the `config.cfg` file. ## FAQ From f16d960feb476f28b8e219a7c9aee6f89f1a77ef Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 17:40:31 +0300 Subject: [PATCH 184/769] additional columns --- .github/ISSUE_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0689e365..d3775717 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,6 +10,7 @@ + ### Summary of the problem @@ -18,6 +19,10 @@ +### The way of deployment (cloud or local) + + + ### Expected behavior @@ -25,3 +30,7 @@ ### Actual behavior + +### Full log + + From abafe1581c6c78d784cf215b214947675d49eff8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 18:04:51 +0300 Subject: [PATCH 185/769] Fixed #147 --- algo | 2 +- deploy.yml | 2 +- playbooks/common.yml | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/algo b/algo index deb070cd..e24d71e5 100755 --- a/algo +++ b/algo @@ -324,7 +324,7 @@ Enter the public IP address of your server: (IMPORTANT! This IP is used to verif ROLES="local vpn" EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" - SKIP_TAGS+=" cloud" + SKIP_TAGS+=" cloud update-alternatives" } algo_provisioning () { diff --git a/deploy.yml b/deploy.yml index b6c8380a..dca53ef2 100644 --- a/deploy.yml +++ b/deploy.yml @@ -22,7 +22,7 @@ pre_tasks: - name: Common pre-tasks include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'pre' ] + tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] - name: DigitalOcean pre-tasks include: playbooks/digitalocean.yml diff --git a/playbooks/common.yml b/playbooks/common.yml index d84a6eb0..eb7a695b 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -3,3 +3,5 @@ - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + tags: + - update-alternatives From d55878147327df3e8902c2b6e18dd90f92ce7b2b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 18:13:58 +0300 Subject: [PATCH 186/769] dirty fix #148 --- playbooks/common.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/common.yml b/playbooks/common.yml index eb7a695b..36a051c6 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -1,5 +1,5 @@ - name: Install prerequisites - raw: sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + raw: sleep 10 && sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 - name: Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 From 0269cafff7554254b89e797f02726279b87cb8ab Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 18:52:34 +0300 Subject: [PATCH 187/769] DNS fix --- config.cfg | 10 ++++++---- roles/vpn/templates/ipsec.conf.j2 | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config.cfg b/config.cfg index 34a1908e..296de4de 100644 --- a/config.cfg +++ b/config.cfg @@ -29,10 +29,12 @@ server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" dns_servers: - - 8.8.8.8 - - 8.8.4.4 - - 2001:4860:4860::8888 - - 2001:4860:4860::8844 + ipv4: + - 8.8.8.8 + - 8.8.4.4 + ipv6: + - 2001:4860:4860::8888 + - 2001:4860:4860::8844 strongswan_enabled_plugins: - aes diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 58089c10..6b60e36e 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -28,7 +28,7 @@ conn %default {% if local_dns is defined and local_dns == "Y" %} rightdns={{ local_service_ip }} {% else %} - rightdns={% for host in dns_servers %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %} + rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support is defined and ipv6_support == "yes" %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} {% endif %} conn ikev2-pubkey From 016a8c770803fe1c28e25ae9f910ba7f347cb478 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 12 Dec 2016 15:14:58 -0500 Subject: [PATCH 188/769] Change default instance to free tier (t2.micro) I know this is a bit goofy, but the t2.nano is not in the free tier for AWS even though it is smaller than the t2.micro instance. See: https://aws.amazon.com/blogs/aws/ec2-update-t2-nano-instances-now-available/ (the "PS" at the bottom), confirmed on pricing page. The difference is $4.30 per mo vs. free/$8.76 per mo. Maybe add this to config questions, but at least one reviewer has noted this as an issue for his just-setup AWS free account. --- roles/cloud-ec2/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index ae4fbab7..0be4f379 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -55,7 +55,7 @@ aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" keypair: "VPNKEY" group: vpn-secgroup - instance_type: t2.nano + instance_type: t2.micro image: "{{ ami_image }}" wait: true region: "{{ region }}" From 48231cf02002942ce06cefb67066d5634379aa8d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 08:44:19 +0300 Subject: [PATCH 189/769] SSH fix for old bash versions #160 --- algo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/algo b/algo index e24d71e5..bd4aaf30 100755 --- a/algo +++ b/algo @@ -93,10 +93,10 @@ Enter your azure subscription_id (https://docs.ansible.com/ansible/guide_azure.h You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_subscription_id - read -e -p " + read -p " -Enter the local path to your SSH public key: -: " -i "~/.ssh/id_rsa.pub" -r ssh_public_key +Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key + ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: From bb90bb26a62ca3c3d775210b35311df2693c0095 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 09:08:12 +0300 Subject: [PATCH 190/769] a fix for ipv6 provisioning on DO #158 --- playbooks/digitalocean.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/playbooks/digitalocean.yml b/playbooks/digitalocean.yml index 703e5d05..78b652b2 100644 --- a/playbooks/digitalocean.yml +++ b/playbooks/digitalocean.yml @@ -1,5 +1,6 @@ - name: Enable IPv6 on the droplet - uri: + local_action: + module: uri url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" method: POST body: @@ -8,15 +9,18 @@ status_code: 201 HEADER_Authorization: "Bearer {{ do_access_token }}" HEADER_Content-Type: "application/json" + become: no - name: Get Droplet networks - uri: + local_action: + module: uri url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" method: GET status_code: 200 HEADER_Authorization: "Bearer {{ do_access_token }}" HEADER_Content-Type: "application/json" register: droplet_info + become: no - name: IPv6 configured template: src=roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 From 4d1c048b8f664f52ff349d6489b3938876b0a633 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Tue, 13 Dec 2016 01:26:02 -0500 Subject: [PATCH 191/769] README: Add apt dependencies for pypi cryptography. The cryptography library requires gcc and some development headers that aren't installed by default on Ubuntu. Source: https://cryptography.io/en/latest/installation/#building-cryptography-on-linux --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5965673..8164cf80 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual 1. Install the dependencies for your operating system: OS X: `sudo easy_install pip && sudo pip install -r requirements.txt` - Linux (deb-based): `sudo easy_install pip && sudo apt-get install libssl-dev && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo easy_install pip && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 3. Start the deploy and follow the instructions: `./algo` From 517366f1941d241ffe252d1d53f1810f380cf3f3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 20:34:27 +0300 Subject: [PATCH 192/769] EC2 fix --- roles/cloud-ec2/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 0795b7f0..ec4b1bf9 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -106,9 +106,9 @@ easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 ipv6_support: no - with_items: "{{ ec2.instances }}" + with_items: "{{ ec2.tagged_instances }}" - name: Wait for SSH to become available local_action: "wait_for port=22 host={{ item.public_dns_name }} timeout=320" - with_items: "{{ ec2.instances }}" + with_items: "{{ ec2.tagged_instances }}" become: false From 37ec574d8d4aa85f0e73c39651c2e3fb0f5afc2f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 20:46:27 +0300 Subject: [PATCH 193/769] IP_subject_alt_name is not declared for localhost. Fixed #149 --- roles/ssh_tunneling/tasks/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index ff787161..b279b021 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -1,5 +1,8 @@ --- +- set_fact: + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + - name: Ensure that the sshd_config file has desired options blockinfile: dest: /etc/ssh/sshd_config From 275663264a7dc8a5b9ec4f733401f9f83e63e177 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 21:12:51 +0300 Subject: [PATCH 194/769] ipv6 option is available in ansible 2.2; Fixed #158 --- playbooks/digitalocean.yml | 40 ------------------------- roles/cloud-digitalocean/tasks/main.yml | 1 + 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 playbooks/digitalocean.yml diff --git a/playbooks/digitalocean.yml b/playbooks/digitalocean.yml deleted file mode 100644 index 78b652b2..00000000 --- a/playbooks/digitalocean.yml +++ /dev/null @@ -1,40 +0,0 @@ -- name: Enable IPv6 on the droplet - local_action: - module: uri - url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}/actions" - method: POST - body: - type: enable_ipv6 - body_format: json - status_code: 201 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - become: no - -- name: Get Droplet networks - local_action: - module: uri - url: "https://api.digitalocean.com/v2/droplets/{{ do_droplet_id }}" - method: GET - status_code: 200 - HEADER_Authorization: "Bearer {{ do_access_token }}" - HEADER_Content-Type: "application/json" - register: droplet_info - become: no - -- name: IPv6 configured - template: src=roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 dest=/etc/network/interfaces.d/20-ipv6.cfg owner=root group=root mode=0644 - with_items: "{{ droplet_info.json.droplet.networks.v6 }}" - notify: - - reload eth0 - -- name: IPv6 included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/20-ipv6.cfg' state=present - notify: - - reload eth0 - -- meta: flush_handlers - -- name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ inventory_hostname }} timeout=320" - become: false diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 5a28f8f7..9fb047d1 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -21,6 +21,7 @@ ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" unique_name: yes api_token: "{{ do_access_token }}" + ipv6: yes register: do - name: Add the droplet to an inventory group From 17bd6c6a0cf33687c8703aa2db24b6b651cbd8e8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 21:27:59 +0300 Subject: [PATCH 195/769] additional fix #158 --- deploy.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deploy.yml b/deploy.yml index dca53ef2..c0bf989a 100644 --- a/deploy.yml +++ b/deploy.yml @@ -24,10 +24,6 @@ include: playbooks/common.yml tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] - - name: DigitalOcean pre-tasks - include: playbooks/digitalocean.yml - tags: [ 'digitalocean' ] - roles: - { role: security, tags: [ 'security' ] } - { role: proxy, tags: [ 'proxy', 'adblock' ] } From 03c805cb8779eb96fd69d2b388a4077dbf4c0f6d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 21:58:45 +0300 Subject: [PATCH 196/769] reorganize the wait_for functions #159 --- deploy.yml | 14 ++++++++++++++ roles/cloud-azure/tasks/main.yml | 4 ++-- roles/cloud-digitalocean/tasks/main.yml | 4 ++-- roles/cloud-ec2/tasks/main.yml | 6 ++---- roles/cloud-gce/tasks/main.yml | 4 ++-- roles/common/tasks/main.yml | 15 +++++++-------- roles/local/tasks/main.yml | 5 ++--- users.yml | 11 +++++++++-- 8 files changed, 40 insertions(+), 23 deletions(-) diff --git a/deploy.yml b/deploy.yml index c0bf989a..e2817ec1 100644 --- a/deploy.yml +++ b/deploy.yml @@ -11,6 +11,20 @@ - { role: cloud-azure, tags: ['azure'] } - { role: local, tags: ['local'] } + post_tasks: + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ cloud_instance_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present + become: false + tags: + - cloud + - name: Post-provisioning tasks hosts: vpn-host gather_facts: false diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index d894b2e5..6c27186d 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -67,5 +67,5 @@ cloud_provider: azure ipv6_support: no -- name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ ip_address }} timeout=320" +- set_fact: + cloud_instance_ip: "{{ ip_address }}" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 9fb047d1..73ebc2f0 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -36,5 +36,5 @@ cloud_provider: digitalocean ipv6_support: yes -- name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ do.droplet.ip_address }} timeout=320" +- set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index ec4b1bf9..8c48019d 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -108,7 +108,5 @@ ipv6_support: no with_items: "{{ ec2.tagged_instances }}" -- name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ item.public_dns_name }} timeout=320" - with_items: "{{ ec2.tagged_instances }}" - become: false +- set_fact: + cloud_instance_ip: "{{ ec2.tagged_instances[0].public_ip }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 959ec6f0..7b88bfd4 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -36,5 +36,5 @@ credentials_file: "{{ credentials_file }}" project_id: "{{ credentials_file_lookup.project_id }}" -- name: Waiting for SSH to become available - local_action: "wait_for port=22 host={{ google_vm.instance_data[0].public_ip }} timeout=320" +- set_fact: + cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index a5730ac1..12d7109c 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -28,15 +28,14 @@ tags: - cloud -- name: Wait for shutdown - local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped timeout=120 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - tags: - - cloud - - name: Wait until SSH becomes ready... - local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=120 + local_action: + module: wait_for + port: 22 + host: "{{ inventory_hostname }}" + search_regex: OpenSSH + delay: 10 + timeout: 320 when: reboot_required is defined and reboot_required.stdout == 'required' become: false tags: diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index 4be24334..b1a73ea9 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -19,6 +19,5 @@ cloud_provider: local when: server_ip == "localhost" -- name: Waiting for SSH to become available - local_action: "wait_for port=22 host={{ server_ip }} timeout=320" - when: server_ip != "localhost" +- set_fact: + cloud_instance_ip: "{{ server_ip }}" diff --git a/users.yml b/users.yml index fb79ba20..7f074bc0 100644 --- a/users.yml +++ b/users.yml @@ -41,8 +41,15 @@ ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" IP_subject: "{{ IP_subject }}" - - name: Wait for SSH to become available - local_action: "wait_for port=22 host={{ server_ip }} timeout=320" + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ server_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present become: false - name: User management From b873da4d89cfc3c5f985d1661bb0e9b3fa406110 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 21:59:31 +0300 Subject: [PATCH 197/769] remove unused handlers --- deploy.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deploy.yml b/deploy.yml index e2817ec1..e3d1efd5 100644 --- a/deploy.yml +++ b/deploy.yml @@ -46,10 +46,6 @@ - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - { role: vpn, tags: [ 'vpn' ] } - handlers: - - name: reload eth0 - shell: sh -c 'ifdown eth0; ip addr flush dev eth0; ifup eth0' - post_tasks: - shell: | echo "#----------------------------------------------------------------------#" From 4419f65b1707251a63d76bbfed3b710c72df9492 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 22:05:55 +0300 Subject: [PATCH 198/769] Additional pause. Fixed #159 --- deploy.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deploy.yml b/deploy.yml index e3d1efd5..2267f2c1 100644 --- a/deploy.yml +++ b/deploy.yml @@ -25,6 +25,12 @@ tags: - cloud + - name: A short pause, in order to be sure the instance is ready + pause: + seconds: 10 + tags: + - cloud + - name: Post-provisioning tasks hosts: vpn-host gather_facts: false From 1164ead6398bffced9708efba06bf4511926256b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 13 Dec 2016 22:08:05 +0300 Subject: [PATCH 199/769] name fixes --- deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy.yml b/deploy.yml index 2267f2c1..01d8af89 100644 --- a/deploy.yml +++ b/deploy.yml @@ -1,4 +1,4 @@ -- name: Configure the server and install required software +- name: Configure the server hosts: localhost tags: algo vars_files: @@ -31,7 +31,7 @@ tags: - cloud -- name: Post-provisioning tasks +- name: Configure the server and install required software hosts: vpn-host gather_facts: false tags: algo From f1715c4e0b501b4e0999417179f0b2b1407fe305 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 14 Dec 2016 18:49:47 +0300 Subject: [PATCH 200/769] random password for the p12 certificates #135 --- config.cfg | 15 +++++++++++---- deploy.yml | 13 +------------ roles/cloud-azure/tasks/main.yml | 1 - roles/cloud-digitalocean/tasks/main.yml | 1 - roles/cloud-ec2/tasks/main.yml | 1 - roles/cloud-gce/tasks/main.yml | 1 - roles/local/tasks/main.yml | 2 -- roles/vpn/tasks/main.yml | 7 ++++--- users.yml | 22 +++++++++++++--------- 9 files changed, 29 insertions(+), 34 deletions(-) diff --git a/config.cfg b/config.cfg index 23865ee5..06917655 100644 --- a/config.cfg +++ b/config.cfg @@ -9,10 +9,6 @@ users: # Avoid using '+' in your email address otherwise auditd will fail to start. auditd_action_mail_acct: email@example.com -# Exported certificates will be protected by the password below: -easyrsa_p12_export_password: vpnpws - - ### Advanced users only below this line ### easyrsa_dir: /opt/easy-rsa-ipsec @@ -74,3 +70,14 @@ local_service_ip: 172.16.0.1 pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" + +congrats: | + "#----------------------------------------------------------------------#" + "# Congratulations! #" + "# Your Algo server is running. #" + "# Config files and certificates are in the ./configs/ directory. #" + "# Go to https://whoer.net/ after connecting #" + "# and ensure that all your traffic passes through the VPN. #" + "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" + "# The p12 password is {{ easyrsa_p12_export_password }}" + "#----------------------------------------------------------------------#" diff --git a/deploy.yml b/deploy.yml index 01d8af89..f8d50710 100644 --- a/deploy.yml +++ b/deploy.yml @@ -53,16 +53,5 @@ - { role: vpn, tags: [ 'vpn' ] } post_tasks: - - shell: | - echo "#----------------------------------------------------------------------#" - echo "# Congratulations! #" - echo "# Your Algo server is running. #" - echo "# Config files and certificates are in the ./configs/ directory. #" - echo "# Go to https://whoer.net/ after connecting #" - echo "# and ensure that all your traffic passes through the VPN. #" - echo "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" - echo "#----------------------------------------------------------------------#" - tags: always - register: congrats - - debug: msg="{{ congrats.stdout_lines }}" + - debug: msg="{{ congrats.split('\n') }}" tags: always diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 6c27186d..abe2134b 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -63,7 +63,6 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: azure ipv6_support: no diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 73ebc2f0..d8dd57cb 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -32,7 +32,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: digitalocean ipv6_support: yes diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 8c48019d..5ff40dce 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -103,7 +103,6 @@ groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: ec2 ipv6_support: no with_items: "{{ ec2.tagged_instances }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 7b88bfd4..c909b3f2 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -20,7 +20,6 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: gce ipv6_support: no diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index b1a73ea9..d2deff6f 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -4,7 +4,6 @@ groups: vpn-host ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: local when: server_ip != "localhost" @@ -15,7 +14,6 @@ ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" ansible_connection: local - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" cloud_provider: local when: server_ip == "localhost" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index f5951a45..8c55e63f 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,6 +1,9 @@ - name: Gather Facts setup: +- set_fact: + easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + - name: Install StrongSwan apt: name=strongswan state=latest update_cache=yes @@ -134,11 +137,9 @@ - name: Build the client's p12 shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:"{{ easyrsa_p12_export_password }}" args: chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' with_items: "{{ users }}" - name: Copy the CA cert to the strongswan directory diff --git a/users.yml b/users.yml index 7f074bc0..ceb460cc 100644 --- a/users.yml +++ b/users.yml @@ -21,11 +21,6 @@ default: "n" private: no - - name: "easyrsa_p12_export_password" - prompt: "Enter a password for p12 certificates and SSH private keys: (minimum five characters)\n" - default: "vpnpw" - private: yes - - name: "IP_subject" prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" private: no @@ -37,7 +32,6 @@ groupname: vpn-host ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" - easyrsa_p12_export_password: "{{ easyrsa_p12_export_password }}" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" IP_subject: "{{ IP_subject }}" @@ -67,6 +61,13 @@ - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } tasks: + + - name: Gather Facts + setup: + + - set_fact: + easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + - name: Build the client's pair shell: > ./easyrsa build-client-full {{ item }} nopass && @@ -78,11 +79,9 @@ - name: Build the client's p12 shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' + openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} args: chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_p12_initialized' with_items: "{{ users }}" - name: Get active users @@ -150,4 +149,9 @@ - name: SSH | Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" with_items: "{{ users }}" + + post_tasks: + - debug: msg="{{ congrats.split('\n') }}" + tags: always From ecb6b498b9a4edff16156e31e244e4f1408d7eff Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 14 Dec 2016 19:42:39 +0300 Subject: [PATCH 201/769] unnecessarry to use such way Fixed #162 --- config.cfg | 8 -------- roles/vpn/templates/client_ipsec.conf.j2 | 9 ++++++--- roles/vpn/templates/ipsec.conf.j2 | 9 ++++++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/config.cfg b/config.cfg index 06917655..792aae70 100644 --- a/config.cfg +++ b/config.cfg @@ -52,14 +52,6 @@ strongswan_enabled_plugins: - stroke - x509 -ipsec_config: - dpdaction: 'clear' - dpddelay: '35s' - rekey: 'no' - keyexchange: 'ikev2' - compress: 'yes' - fragmentation: 'yes' - ec2_vpc_nets: cidr_block: 172.251.0.0/23 subnet_cidr: 172.251.1.0/24 diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 2e97c36b..32a71f79 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -1,7 +1,10 @@ conn ikev2-{{ IP_subject_alt_name }} -{% for key, value in ipsec_config.iteritems() %} - {{ key }}={{ value }} -{% endfor %} + fragmentation=yes + rekey=no + dpdaction=clear + keyexchange=ikev2 + compress=yes + dpddelay=35s {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 6b60e36e..1b3aa7f5 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -3,9 +3,12 @@ config setup charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" conn %default -{% for key, value in ipsec_config.iteritems() %} - {{ key }}={{ value }} -{% endfor %} + fragmentation=yes + rekey=no + dpdaction=clear + keyexchange=ikev2 + compress=yes + dpddelay=35s {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! From 8b0fe4d8f33cde2ba9a91e2a09d607914711e95b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 14 Dec 2016 21:54:14 +0300 Subject: [PATCH 202/769] Block client-to-client traffic. Fixed #166 --- algo | 6 ++++++ roles/vpn/templates/rules.v4.j2 | 3 +++ roles/vpn/templates/rules.v6.j2 | 3 +++ 3 files changed, 12 insertions(+) diff --git a/algo b/algo index bd4aaf30..8a0fe6d0 100755 --- a/algo +++ b/algo @@ -61,6 +61,12 @@ Do you want to enable VPN for Windows 10 clients? (Will use insecure algorithms Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi +read -p " +Do you want to block client-to-client traffic? +[y/N]: " -r BetweenClients_DROP +BetweenClients_DROP=${BetweenClients_DROP:-n} +if [[ "$BetweenClients_DROP" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" BetweenClients_DROP=Y"; fi + } deploy () { diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index c8dc1ded..d793fe13 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -21,6 +21,9 @@ COMMIT # particular virtual (tun,tap,...) or physical (ethernet) interface. -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT -A INPUT -d {{ local_service_ip }} -p tcp -m multiport --dport 8080,8118 -j ACCEPT +{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +-A FORWARD -s {{ vpn_network }} -d {{ vpn_network }} -j DROP +{% endif %} -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 71342a0d..c70dc327 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -26,6 +26,9 @@ COMMIT # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT +{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +-A FORWARD -s {{ vpn_network_ipv6 }} -d {{ vpn_network_ipv6 }} -j DROP +{% endif %} -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP From d5545b974caa0fadfbd82bfe8f8be642f16b0af6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 22:02:45 +0300 Subject: [PATCH 203/769] generating ssh-keys #152 #151 #112 --- ansible.cfg | 2 +- config.cfg | 5 ++++ deploy.yml | 5 ++++ playbooks/local.yml | 14 ++++++++++ roles/cloud-azure/tasks/main.yml | 3 ++- roles/cloud-digitalocean/tasks/main.yml | 36 ++++++++++++++++++++++++- roles/cloud-ec2/tasks/main.yml | 3 ++- roles/cloud-gce/tasks/main.yml | 3 ++- 8 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 playbooks/local.yml diff --git a/ansible.cfg b/ansible.cfg index 1a3afab2..03037011 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -8,5 +8,5 @@ host_key_checking = False record_host_keys = False [ssh_connection] -ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 -o IdentitiesOnly=yes scp_if_ssh = True diff --git a/config.cfg b/config.cfg index 792aae70..158f5b6b 100644 --- a/config.cfg +++ b/config.cfg @@ -73,3 +73,8 @@ congrats: | "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" "# The p12 password is {{ easyrsa_p12_export_password }}" "#----------------------------------------------------------------------#" + +SSH_keys: + comment: algo@ssh + private: configs/algo.pem + public: configs/algo.pem.pub diff --git a/deploy.yml b/deploy.yml index f8d50710..a94cc49e 100644 --- a/deploy.yml +++ b/deploy.yml @@ -4,6 +4,11 @@ vars_files: - config.cfg + pre_tasks: + - name: Local pre-tasks + include: playbooks/local.yml + tags: [ 'cloud' ] + roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } - { role: cloud-ec2, tags: ['ec2'] } diff --git a/playbooks/local.yml b/playbooks/local.yml new file mode 100644 index 00000000..a7bc353e --- /dev/null +++ b/playbooks/local.yml @@ -0,0 +1,14 @@ +--- + +- name: Generate the SSH private key + local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" + args: + creates: configs/algo.pem + +- name: Generate the SSH public key + local_action: shell echo `ssh-keygen -y -f configs/algo.pem` {{ SSH_keys.comment }} > {{ SSH_keys.public }} + args: + creates: configs/algo.pem.pub + +- name: Change mode for the SSH private key + local_action: file path=configs/algo.pem mode=0600 diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index abe2134b..14f34f26 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -46,7 +46,7 @@ tags: service: algo ssh_public_keys: - - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ ssh_public_key }}') }}" } + - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } image: offer: UbuntuServer publisher: Canonical @@ -63,6 +63,7 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: azure ipv6_support: no diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index d8dd57cb..fe506984 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,12 +1,45 @@ - name: Set the DigitalOcean Access Token fact set_fact: do_token: "{{ do_access_token }}" + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + +- name: Get existing SSH keys + uri: + url: https://api.digitalocean.com/v2/account/keys + method: GET + HEADER_Content-Type: 'application/json' + HEADER_Authorization: "Bearer {{ do_access_token }}" + status_code: 200 + body_format: json + register: do_existing_keys + +- set_fact: + ssh_key_exist: true + when: public_key == item.public_key + with_items: + - "{{ do_existing_keys.json.ssh_keys }}" + +- name: Upload the SSH key + uri: + url: https://api.digitalocean.com/v2/account/keys + method: POST + HEADER_Content-Type: 'application/json' + HEADER_Authorization: "Bearer {{ do_access_token }}" + body: > + { + "name" : "{{ SSH_keys.comment }}", + "public_key" : "{{ public_key }}" + } + status_code: 201 + body_format: json + register: do_ssh_key + when: ssh_key_exist is not defined - name: "Getting your SSH key ID on Digital Ocean..." digital_ocean: state: present command: ssh - name: "{{ do_ssh_name }}" + name: "{{ SSH_keys.comment }}" api_token: "{{ do_access_token }}" register: do_ssh_key @@ -30,6 +63,7 @@ groups: vpn-host ansible_ssh_user: root ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" cloud_provider: digitalocean diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 5ff40dce..41f46bc1 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -20,7 +20,7 @@ name: VPNKEY region: "{{ region }}" key_material: "{{ item }}" - with_file: "{{ ssh_public_key }}" + with_file: "{{ SSH_keys.public }}" register: keypair - name: Configure EC2 virtual private clouds @@ -103,6 +103,7 @@ groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: ec2 ipv6_support: no with_items: "{{ ec2.tagged_instances }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index c909b3f2..3f4d20ea 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,6 +1,6 @@ - set_fact: credentials_file_lookup: "{{ lookup('file', '{{ credentials_file }}') }}" - ssh_public_key_lookup: "{{ lookup('file', '{{ ssh_public_key }}') }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - name: "Creating a new instance..." gce: @@ -20,6 +20,7 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: gce ipv6_support: no From d51abd21d1cd561c217743af3cece9b4427fcaa2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 15 Dec 2016 00:21:44 +0300 Subject: [PATCH 204/769] some fixes --- algo | 17 ----------------- roles/cloud-digitalocean/tasks/main.yml | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/algo b/algo index 8a0fe6d0..b75317af 100755 --- a/algo +++ b/algo @@ -99,10 +99,6 @@ Enter your azure subscription_id (https://docs.ansible.com/ansible/guide_azure.h You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_subscription_id - read -p " - -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: @@ -155,10 +151,6 @@ digitalocean () { Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): : " -rs do_access_token - read -p " -Enter an existing SSH key name (https://cloud.digitalocean.com/settings/security): -: " -r do_ssh_name - read -p " Name the vpn server: [algo.local]: " -r do_server_name @@ -212,11 +204,6 @@ Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [ABCD...]: " -rs aws_secret_key - - read -p " -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} - read -p " Name the vpn server: [algo]: " -r aws_server_name @@ -266,10 +253,6 @@ gce () { Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): : " -r credentials_file - read -p " -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} - read -p " Name the vpn server: [algo]: " -r server_name diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index fe506984..55e7850b 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -39,7 +39,7 @@ digital_ocean: state: present command: ssh - name: "{{ SSH_keys.comment }}" + ssh_pub_key: "{{ public_key }}" api_token: "{{ do_access_token }}" register: do_ssh_key From abf94989fc1af52fd42ccb2e8f67db4a34a84c50 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 15 Dec 2016 13:33:29 +0300 Subject: [PATCH 205/769] the password for the CA private key #75 --- config.cfg | 5 +++-- roles/vpn/tasks/main.yml | 19 +++++++++++++++---- users.yml | 10 +++++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/config.cfg b/config.cfg index 792aae70..aaddfa02 100644 --- a/config.cfg +++ b/config.cfg @@ -70,6 +70,7 @@ congrats: | "# Config files and certificates are in the ./configs/ directory. #" "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }}" - "# The p12 password is {{ easyrsa_p12_export_password }}" + "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} " + "# The p12 password is {{ easyrsa_p12_export_password }} " + "# The CA key password is {{ easyrsa_CA_password }} " "#----------------------------------------------------------------------#" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 8c55e63f..8bbb4416 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,8 +1,14 @@ - name: Gather Facts setup: +- name: Generate password for the CA key + shell: > + < /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-12};echo; + register: CA_password + - set_fact: easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + easyrsa_CA_password: "{{ CA_password.stdout }}" - name: Install StrongSwan apt: name=strongswan state=latest update_cache=yes @@ -89,7 +95,10 @@ with_items: "{{ strongswan_plugins.stdout_lines }}" - name: Fetch easy-rsa-ipsec from git - git: repo=git://github.com/ValdikSS/easy-rsa-ipsec.git version=ed4de10d7ce0726357fb1bb4729f8eb440c06e2b dest="{{ easyrsa_dir }}" + git: + repo: git://github.com/ValdikSS/easy-rsa-ipsec.git + version: ipsec-with-patches + dest: "{{ easyrsa_dir }}" - name: Setup the vars file from our template template: src=easy-rsa.vars.j2 dest={{ easyrsa_dir }}/easyrsa3/vars @@ -108,7 +117,7 @@ - name: Build the CA pair shell: > - ./easyrsa build-ca nopass && + ./easyrsa --batch build-ca -- -passout pass:"{{ easyrsa_CA_password }}" && touch {{ easyrsa_dir }}/easyrsa3/pki/ca_initialized args: chdir: '{{ easyrsa_dir }}/easyrsa3/' @@ -118,7 +127,8 @@ - name: Build the server pair shell: > - ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' build-server-full {{ IP_subject_alt_name }} nopass && + ./easyrsa gen-req {{ IP_subject_alt_name }} batch nopass -- -passin pass:qwe1 -subj "/CN={{ IP_subject_alt_name }}" && + ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' sign-req server {{ IP_subject_alt_name }} -- -passin pass:"{{ easyrsa_CA_password }}" && touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' @@ -128,7 +138,8 @@ - name: Build the client's pair shell: > - ./easyrsa build-client-full {{ item }} nopass && + ./easyrsa gen-req {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + ./easyrsa --subject-alt-name='DNS:{{ item }}' sign-req client {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" && touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' diff --git a/users.yml b/users.yml index ceb460cc..b6f71307 100644 --- a/users.yml +++ b/users.yml @@ -25,6 +25,10 @@ prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" private: no + - name: "easyrsa_CA_password" + prompt: "Enter the password for the private CA key:\n" + private: yes + tasks: - name: Add the server to the vpn-host group add_host: @@ -33,6 +37,7 @@ ansible_ssh_user: "{{ server_user }}" ansible_python_interpreter: "/usr/bin/python2.7" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" + easyrsa_CA_password: "{{ easyrsa_CA_password }}" IP_subject: "{{ IP_subject }}" - name: Wait until SSH becomes ready... @@ -70,7 +75,8 @@ - name: Build the client's pair shell: > - ./easyrsa build-client-full {{ item }} nopass && + ./easyrsa gen-req {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + ./easyrsa --subject-alt-name='DNS:{{ item }}' sign-req client {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" && touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' args: chdir: '{{ easyrsa_dir }}/easyrsa3/' @@ -93,8 +99,10 @@ - name: Revoke non-existing users shell: > + openssl ec -in pki/private/ca.key -out pki/private/ca.key -passin pass:"{{ easyrsa_CA_password }}" -passout pass:"" && ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && ./easyrsa revoke {{ item }} && + openssl ec -aes256 -in pki/private/ca.key -out pki/private/ca.key -passin pass:"" -passout pass:"{{ easyrsa_CA_password }}" && ipsec rereadcrls args: chdir: '{{ easyrsa_dir }}/easyrsa3/' From efb78e27d4e2ee0f946c39ac679e6db89e55a0a9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 16 Dec 2016 22:30:07 +0300 Subject: [PATCH 206/769] disable the proxy and client-to-client options --- algo | 12 ------------ config.cfg | 4 ++++ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/algo b/algo index 8a0fe6d0..a25a75ed 100755 --- a/algo +++ b/algo @@ -11,12 +11,6 @@ Do you want to apply security enhancements? security_enabled=${security_enabled:-n} if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi -read -p " -Do you want to install an HTTP proxy to block ads and decrease traffic usage while surfing? -[y/N]: " -r proxy_enabled -proxy_enabled=${proxy_enabled:-n} -if [[ "$proxy_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" proxy"; fi - read -p " Do you want to install a local DNS resolver to block ads while surfing? [y/N]: " -r dns_enabled @@ -61,12 +55,6 @@ Do you want to enable VPN for Windows 10 clients? (Will use insecure algorithms Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi -read -p " -Do you want to block client-to-client traffic? -[y/N]: " -r BetweenClients_DROP -BetweenClients_DROP=${BetweenClients_DROP:-n} -if [[ "$BetweenClients_DROP" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" BetweenClients_DROP=Y"; fi - } deploy () { diff --git a/config.cfg b/config.cfg index aaddfa02..b6f93564 100644 --- a/config.cfg +++ b/config.cfg @@ -63,6 +63,10 @@ pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" +# Block traffic between connected clients + +BetweenClients_DROP: Y + congrats: | "#----------------------------------------------------------------------#" "# Congratulations! #" From 1d07200c74802b62ffac84f4a1d8c46db09e7cda Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 12 Dec 2016 22:02:45 +0300 Subject: [PATCH 207/769] generating ssh-keys #152 #151 #112 --- ansible.cfg | 2 +- config.cfg | 5 ++++ deploy.yml | 5 ++++ playbooks/local.yml | 14 ++++++++++ roles/cloud-azure/tasks/main.yml | 3 ++- roles/cloud-digitalocean/tasks/main.yml | 36 ++++++++++++++++++++++++- roles/cloud-ec2/tasks/main.yml | 3 ++- roles/cloud-gce/tasks/main.yml | 3 ++- 8 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 playbooks/local.yml diff --git a/ansible.cfg b/ansible.cfg index 1a3afab2..03037011 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -8,5 +8,5 @@ host_key_checking = False record_host_keys = False [ssh_connection] -ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 -o IdentitiesOnly=yes scp_if_ssh = True diff --git a/config.cfg b/config.cfg index b6f93564..7a0bc3df 100644 --- a/config.cfg +++ b/config.cfg @@ -78,3 +78,8 @@ congrats: | "# The p12 password is {{ easyrsa_p12_export_password }} " "# The CA key password is {{ easyrsa_CA_password }} " "#----------------------------------------------------------------------#" + +SSH_keys: + comment: algo@ssh + private: configs/algo.pem + public: configs/algo.pem.pub diff --git a/deploy.yml b/deploy.yml index f8d50710..a94cc49e 100644 --- a/deploy.yml +++ b/deploy.yml @@ -4,6 +4,11 @@ vars_files: - config.cfg + pre_tasks: + - name: Local pre-tasks + include: playbooks/local.yml + tags: [ 'cloud' ] + roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } - { role: cloud-ec2, tags: ['ec2'] } diff --git a/playbooks/local.yml b/playbooks/local.yml new file mode 100644 index 00000000..a7bc353e --- /dev/null +++ b/playbooks/local.yml @@ -0,0 +1,14 @@ +--- + +- name: Generate the SSH private key + local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" + args: + creates: configs/algo.pem + +- name: Generate the SSH public key + local_action: shell echo `ssh-keygen -y -f configs/algo.pem` {{ SSH_keys.comment }} > {{ SSH_keys.public }} + args: + creates: configs/algo.pem.pub + +- name: Change mode for the SSH private key + local_action: file path=configs/algo.pem mode=0600 diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index abe2134b..14f34f26 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -46,7 +46,7 @@ tags: service: algo ssh_public_keys: - - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ ssh_public_key }}') }}" } + - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } image: offer: UbuntuServer publisher: Canonical @@ -63,6 +63,7 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: azure ipv6_support: no diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index d8dd57cb..fe506984 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,12 +1,45 @@ - name: Set the DigitalOcean Access Token fact set_fact: do_token: "{{ do_access_token }}" + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + +- name: Get existing SSH keys + uri: + url: https://api.digitalocean.com/v2/account/keys + method: GET + HEADER_Content-Type: 'application/json' + HEADER_Authorization: "Bearer {{ do_access_token }}" + status_code: 200 + body_format: json + register: do_existing_keys + +- set_fact: + ssh_key_exist: true + when: public_key == item.public_key + with_items: + - "{{ do_existing_keys.json.ssh_keys }}" + +- name: Upload the SSH key + uri: + url: https://api.digitalocean.com/v2/account/keys + method: POST + HEADER_Content-Type: 'application/json' + HEADER_Authorization: "Bearer {{ do_access_token }}" + body: > + { + "name" : "{{ SSH_keys.comment }}", + "public_key" : "{{ public_key }}" + } + status_code: 201 + body_format: json + register: do_ssh_key + when: ssh_key_exist is not defined - name: "Getting your SSH key ID on Digital Ocean..." digital_ocean: state: present command: ssh - name: "{{ do_ssh_name }}" + name: "{{ SSH_keys.comment }}" api_token: "{{ do_access_token }}" register: do_ssh_key @@ -30,6 +63,7 @@ groups: vpn-host ansible_ssh_user: root ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" cloud_provider: digitalocean diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 5ff40dce..41f46bc1 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -20,7 +20,7 @@ name: VPNKEY region: "{{ region }}" key_material: "{{ item }}" - with_file: "{{ ssh_public_key }}" + with_file: "{{ SSH_keys.public }}" register: keypair - name: Configure EC2 virtual private clouds @@ -103,6 +103,7 @@ groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: ec2 ipv6_support: no with_items: "{{ ec2.tagged_instances }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index c909b3f2..3f4d20ea 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,6 +1,6 @@ - set_fact: credentials_file_lookup: "{{ lookup('file', '{{ credentials_file }}') }}" - ssh_public_key_lookup: "{{ lookup('file', '{{ ssh_public_key }}') }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - name: "Creating a new instance..." gce: @@ -20,6 +20,7 @@ groups: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: gce ipv6_support: no From 90cc5fa1f72eb14b3559cd521128ef8cb3b606b4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 15 Dec 2016 00:21:44 +0300 Subject: [PATCH 208/769] some fixes --- algo | 17 ----------------- roles/cloud-digitalocean/tasks/main.yml | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/algo b/algo index a25a75ed..d7d78e70 100755 --- a/algo +++ b/algo @@ -87,10 +87,6 @@ Enter your azure subscription_id (https://docs.ansible.com/ansible/guide_azure.h You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_subscription_id - read -p " - -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} read -p " Name the vpn server: @@ -143,10 +139,6 @@ digitalocean () { Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): : " -rs do_access_token - read -p " -Enter an existing SSH key name (https://cloud.digitalocean.com/settings/security): -: " -r do_ssh_name - read -p " Name the vpn server: [algo.local]: " -r do_server_name @@ -200,11 +192,6 @@ Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached [ABCD...]: " -rs aws_secret_key - - read -p " -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} - read -p " Name the vpn server: [algo]: " -r aws_server_name @@ -254,10 +241,6 @@ gce () { Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): : " -r credentials_file - read -p " -Enter the local path to your SSH public key (~/.ssh/id_rsa.pub): " -r ssh_public_key - ssh_public_key=${ssh_public_key:-$HOME/.ssh/id_rsa.pub} - read -p " Name the vpn server: [algo]: " -r server_name diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index fe506984..55e7850b 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -39,7 +39,7 @@ digital_ocean: state: present command: ssh - name: "{{ SSH_keys.comment }}" + ssh_pub_key: "{{ public_key }}" api_token: "{{ do_access_token }}" register: do_ssh_key From cd5b096ab73f4ea976fe72bf62d41dc83584965d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 17 Dec 2016 15:16:40 +0300 Subject: [PATCH 209/769] DO fix --- roles/cloud-digitalocean/tasks/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 55e7850b..6faa173a 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -41,6 +41,7 @@ command: ssh ssh_pub_key: "{{ public_key }}" api_token: "{{ do_access_token }}" + name: "{{ SSH_keys.comment }}" register: do_ssh_key - name: "Creating a droplet..." From 6d166fe7ccd6e2324f42532e60bad63b89f35af3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 17 Dec 2016 15:26:14 +0300 Subject: [PATCH 210/769] modify requirements #129 --- README.md | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8164cf80..69e11398 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual 1. Install the dependencies for your operating system: - OS X: `sudo easy_install pip && sudo pip install -r requirements.txt` + OS X: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` Linux (deb-based): `sudo easy_install pip && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. diff --git a/requirements.txt b/requirements.txt index 3039915a..2aa7e050 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ boto azure==2.0.0rc5 apache-libcloud six +pyopenssl From 2c9c3ccb091c5a2d2b99c4dff89443b9fea5cc0c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 17 Dec 2016 16:36:59 +0300 Subject: [PATCH 211/769] Fixed #146 --- roles/vpn/templates/rules.v4.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index d793fe13..2a4bb5de 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -15,6 +15,7 @@ COMMIT -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +-A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any From 33b3af540a0a2cf28f0149d7bdc99f779fe6d233 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 19 Dec 2016 00:19:26 +0300 Subject: [PATCH 212/769] Fix SSH keys for DigitalOcean --- roles/cloud-digitalocean/tasks/main.yml | 43 +++++++------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 6faa173a..34614855 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -3,39 +3,18 @@ do_token: "{{ do_access_token }}" public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" -- name: Get existing SSH keys - uri: - url: https://api.digitalocean.com/v2/account/keys - method: GET - HEADER_Content-Type: 'application/json' - HEADER_Authorization: "Bearer {{ do_access_token }}" - status_code: 200 - body_format: json - register: do_existing_keys +- name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_access_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != 1 + retries: 10 + delay: 1 -- set_fact: - ssh_key_exist: true - when: public_key == item.public_key - with_items: - - "{{ do_existing_keys.json.ssh_keys }}" - -- name: Upload the SSH key - uri: - url: https://api.digitalocean.com/v2/account/keys - method: POST - HEADER_Content-Type: 'application/json' - HEADER_Authorization: "Bearer {{ do_access_token }}" - body: > - { - "name" : "{{ SSH_keys.comment }}", - "public_key" : "{{ public_key }}" - } - status_code: 201 - body_format: json - register: do_ssh_key - when: ssh_key_exist is not defined - -- name: "Getting your SSH key ID on Digital Ocean..." +- name: "Upload the SSH key" digital_ocean: state: present command: ssh From 400dfdcc4bad4b0790dc7c0774c668d284db5187 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 19 Dec 2016 14:14:26 -0500 Subject: [PATCH 213/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8074f82a..1b5b936c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ * Please review the [FAQ](https://github.com/trailofbits/algo#faq) * Please include the full output from your terminal window if appropriate +* We only support macOS 10.11 and newer ### Pull Requests From 0ef1b5d8daf7c3584924e1b96f51601c3cb3f261 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 15:08:56 -0500 Subject: [PATCH 214/769] UI hints to ssh keys and message clean up Though the algo ssh key names are in the config file at the bottom, they don't seem to be displayed, and are easy to miss for new users. --- config.cfg | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config.cfg b/config.cfg index 7a0bc3df..297565d3 100644 --- a/config.cfg +++ b/config.cfg @@ -74,9 +74,10 @@ congrats: | "# Config files and certificates are in the ./configs/ directory. #" "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} " - "# The p12 password is {{ easyrsa_p12_export_password }} " - "# The CA key password is {{ easyrsa_CA_password }} " + "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" + "# The p12 password is {{ easyrsa_p12_export_password }} #" + "# The CA key password is {{ easyrsa_CA_password }} #" + "# Shell access: ssh -i algo.pem root@{{ ansible_ssh_host }} #" "#----------------------------------------------------------------------#" SSH_keys: From 8a4057590ceafb4845675f1b57752c1a7cb3ce23 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 15:14:05 -0500 Subject: [PATCH 215/769] UI hints on entering API secrets It's not obvious to new users why some fields display and others are blank when entering values. Absent stars for secrets, this gives a small sanity nudge, and lessens likelihood of double pastes. --- algo | 3 +++ 1 file changed, 3 insertions(+) diff --git a/algo b/algo index d7d78e70..0c4f0666 100755 --- a/algo +++ b/algo @@ -137,6 +137,7 @@ Enter the number of your desired region: digitalocean () { read -p " Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): +[pasted values will not be displayed] : " -rs do_access_token read -p " @@ -185,11 +186,13 @@ ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached +[pasted values will not be displayed] [AKIA...]: " -rs aws_access_key read -p " Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached +[pasted values will not be displayed] [ABCD...]: " -rs aws_secret_key read -p " From d2aa52f4e9af2c2b7cf3c4b46b38396679a90cd9 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 15:21:02 -0500 Subject: [PATCH 216/769] UX hint on profile name Add explicit label for Algo-generated VPNs. If the user has multiple (non-Algo) VPNs for home/office, there is typically a label other than an IP address and "IKEv2". This can be seen, for example, on OSX on the top menu bar for networks. --- roles/vpn/templates/mobileconfig.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 1ccb0374..c118e5f1 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -138,9 +138,9 @@
UserDefinedName {% if proxy_enabled is defined and proxy_enabled == true %} - {{ IP_subject_alt_name }} IKEv2 with proxy + Algo VPN {{ IP_subject_alt_name }} IKEv2 with proxy {% else %} - {{ IP_subject_alt_name }} IKEv2 + Algo VPN {{ IP_subject_alt_name }} IKEv2 {% endif %} VPNType IKEv2 From 00864a7da376ea2ed4298ab766582d26efb3d86e Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 16:08:41 -0500 Subject: [PATCH 217/769] Notes & recipe to install from RH/Cent 6.8 VMs It was very difficult to satisfy all the library dependencies, particularly for Digital Ocean ("dopy") and pycrypto ("cryptography") on RPM-based distros, particularly with the default version of Python that ships with the 6.x line. These steps allow an end-to-end install (verified on Digital Ocean and EC2) with zero warnings or errors. --- docs/Pre-install_steps_RedHat_CentOS_6.x.md | 61 +++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/Pre-install_steps_RedHat_CentOS_6.x.md diff --git a/docs/Pre-install_steps_RedHat_CentOS_6.x.md b/docs/Pre-install_steps_RedHat_CentOS_6.x.md new file mode 100644 index 00000000..a01a121d --- /dev/null +++ b/docs/Pre-install_steps_RedHat_CentOS_6.x.md @@ -0,0 +1,61 @@ +# Algo pre-install steps for Red Hat/CentOS 6.x (currently 6.8) + +There is still heavy use of RH/CentOS 6 (which are essentially the basis of Amazon Linux as well) due to stability and lack of systemd. But unfortunately, as a result there are a number of dated libraries, including python 2.6 and limitations. This script will allow end-to-end installation of Algo on a local (or cloud-based) RH/Cent 6 VM to deploy to cloud instances including Digital Ocean and AWS, with zero warnings or errors. + +## Step 1: Prep for RH/CentOS 6.8/Amazon + +``` +yum -y -q update +yum -y -q install epel-release +``` + +Enable any kernel updates: + +``reboot`` + +## Step 2: Install Ansible & launch Algo + +Fix GPG key warnings during Ansible rpm install + +``rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6`` + +Fix GPG key warning during offical Software Collections (SCL) package install + +``rpm --import https://raw.githubusercontent.com/sclorg/centos-release-scl/master/centos-release-scl/RPM-GPG-KEY-CentOS-SIG-SCLo`` + +RH/Cent 6.x uses Python 2.6 by default, which is explicitly deprecated and produces many warnings and errors, so we will install a safe, non-invasive 2.7 tool set which has to be expressly enabled (and will not survive login sessions and reboots) + +``` +yum -y -q install centos-release-SCL # install Software Collections Library (to enable Python 2.7) +# Won't take effect until explicitly enabled, per login session +yum -y -q install python27-python-devel python27-python-setuptools python27-python-pip +yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel wget unzip ansible nano + +# Enable 2.7 default for this session (needs re-run beween logins & reboots) +# shellcheck disable=SC1091 +source /opt/rh/python27/enable +# We're now defaulted to 2.7 + +pip -q install --upgrade pip # upgrade pip itself +pip -q install pycrypto # python-devel needed to prevent setup.py crash, pycrypto 2.7.1 needed for latest security patch +pip -q install setuptools --upgrade + +wget -q https://github.com/trailofbits/algo/archive/master.zip +unzip master.zip +cd algo-master || echo "No Algo directory found" && exit + +# Must be run from algo-master dir: +pip -q install -r requirements.txt # install Algo local (pusher) dependencies + +nano config.cfg +./algo +``` + +## Post-install OSX: + +* Copy ./configs/*mobileconfig to your local Mac +* Install the VPN profile on your Mac (10.10+ required) + * ``/usr/bin/profiles -I -F ./x.x.x.x_NAME.mobileconfig`` +* To remove: ```/usr/bin/profiles -D -F ./x.x.x.x_NAME.mobileconfig``` + +The VPN connection will now appear under Networks (which can be pinned to the top menu bar if preferred) From 6522afde8ca9440a1cc142da2252e70d824fa5fc Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 16:13:39 -0500 Subject: [PATCH 218/769] Rename Pre-install_steps_RedHat_CentOS_6.x.md to pre-install_redhat_centos_6.x.md --- ...teps_RedHat_CentOS_6.x.md => pre-install_redhat_centos_6.x.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{Pre-install_steps_RedHat_CentOS_6.x.md => pre-install_redhat_centos_6.x.md} (100%) diff --git a/docs/Pre-install_steps_RedHat_CentOS_6.x.md b/docs/pre-install_redhat_centos_6.x.md similarity index 100% rename from docs/Pre-install_steps_RedHat_CentOS_6.x.md rename to docs/pre-install_redhat_centos_6.x.md From 7b7185fcd237417de1a7fb15c2feae9f00fb7f08 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 16:22:27 -0500 Subject: [PATCH 219/769] Update pre-install_redhat_centos_6.x.md --- docs/pre-install_redhat_centos_6.x.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pre-install_redhat_centos_6.x.md b/docs/pre-install_redhat_centos_6.x.md index a01a121d..700ccd22 100644 --- a/docs/pre-install_redhat_centos_6.x.md +++ b/docs/pre-install_redhat_centos_6.x.md @@ -42,7 +42,7 @@ pip -q install setuptools --upgrade wget -q https://github.com/trailofbits/algo/archive/master.zip unzip master.zip -cd algo-master || echo "No Algo directory found" && exit +cd algo-master || echo "No Algo directory found" # Must be run from algo-master dir: pip -q install -r requirements.txt # install Algo local (pusher) dependencies From eb81b0d4c44158333abb9cefede9a01d01db28b4 Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 19:48:37 -0500 Subject: [PATCH 220/769] Remove hardcoded ssh key & username in congrats text --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 297565d3..e62223db 100644 --- a/config.cfg +++ b/config.cfg @@ -77,7 +77,7 @@ congrats: | "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" "# The p12 password is {{ easyrsa_p12_export_password }} #" "# The CA key password is {{ easyrsa_CA_password }} #" - "# Shell access: ssh -i algo.pem root@{{ ansible_ssh_host }} #" + "# Shell access: ssh -i {{ ansible_ssh_private_key_file }} {{ ansible_ssh_user }}@{{ ansible_ssh_host }} #" "#----------------------------------------------------------------------#" SSH_keys: From b266f37f1c52918f96be2e099f02b8e7547aa3cb Mon Sep 17 00:00:00 2001 From: kennwhite Date: Mon, 19 Dec 2016 20:19:00 -0500 Subject: [PATCH 221/769] Formatting fixes --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index e62223db..e27c4555 100644 --- a/config.cfg +++ b/config.cfg @@ -77,7 +77,7 @@ congrats: | "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" "# The p12 password is {{ easyrsa_p12_export_password }} #" "# The CA key password is {{ easyrsa_CA_password }} #" - "# Shell access: ssh -i {{ ansible_ssh_private_key_file }} {{ ansible_ssh_user }}@{{ ansible_ssh_host }} #" + "# Shell access: ssh -i {{ ansible_ssh_private_key_file }} {{ ansible_ssh_user }}@{{ ansible_ssh_host }} #" "#----------------------------------------------------------------------#" SSH_keys: From 75194675ebb97870ab73de22014c5399d054606c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 20 Dec 2016 20:28:13 -0500 Subject: [PATCH 222/769] closes #175 --- roles/cloud-gce/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 3f4d20ea..e94d0d55 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -6,7 +6,7 @@ gce: instance_names: "{{ server_name }}" zone: "{{ zone }}" - machine_type: n1-standard-1 + machine_type: f1-micro image: ubuntu-1604 service_account_email: "{{ credentials_file_lookup.client_email }}" credentials_file: "{{ credentials_file }}" From 3d28bce00fce3e1365cdcea9859e3c6f5705cace Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 20 Dec 2016 20:41:03 -0500 Subject: [PATCH 223/769] Tidy this up --- docs/pre-install_redhat_centos_6.x.md | 28 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/pre-install_redhat_centos_6.x.md b/docs/pre-install_redhat_centos_6.x.md index 700ccd22..4371a8f6 100644 --- a/docs/pre-install_redhat_centos_6.x.md +++ b/docs/pre-install_redhat_centos_6.x.md @@ -1,6 +1,6 @@ # Algo pre-install steps for Red Hat/CentOS 6.x (currently 6.8) -There is still heavy use of RH/CentOS 6 (which are essentially the basis of Amazon Linux as well) due to stability and lack of systemd. But unfortunately, as a result there are a number of dated libraries, including python 2.6 and limitations. This script will allow end-to-end installation of Algo on a local (or cloud-based) RH/Cent 6 VM to deploy to cloud instances including Digital Ocean and AWS, with zero warnings or errors. +Many people prefer RedHat or CentOS 6 (or similar variants like Amazon Linux) for to their stability and lack of systemd. Unfortunately, there are a number of dated libraries, notably Python 2.6, that prevent Algo from running without errors. This script will prepare a RedHat, CentOS, or similar VM to deploy to Algo cloud instances. ## Step 1: Prep for RH/CentOS 6.8/Amazon @@ -15,19 +15,21 @@ Enable any kernel updates: ## Step 2: Install Ansible & launch Algo -Fix GPG key warnings during Ansible rpm install +Fix GPG key warnings during Ansible rpm install: ``rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6`` -Fix GPG key warning during offical Software Collections (SCL) package install +Fix GPG key warning during offical Software Collections (SCL) package install: ``rpm --import https://raw.githubusercontent.com/sclorg/centos-release-scl/master/centos-release-scl/RPM-GPG-KEY-CentOS-SIG-SCLo`` -RH/Cent 6.x uses Python 2.6 by default, which is explicitly deprecated and produces many warnings and errors, so we will install a safe, non-invasive 2.7 tool set which has to be expressly enabled (and will not survive login sessions and reboots) +RedHat/CentOS 6.x uses Python 2.6 by default, which is explicitly deprecated and produces many warnings and errors, so we must install a safe, non-invasive 2.7 tool set which has to be expressly enabled (and will not survive login sessions and reboots): ``` -yum -y -q install centos-release-SCL # install Software Collections Library (to enable Python 2.7) -# Won't take effect until explicitly enabled, per login session +# Install the Software Collections Library (to enable Python 2.7) +yum -y -q install centos-release-SCL + +# 2.7 will not be used until explicitly enabled, per login session yum -y -q install python27-python-devel python27-python-setuptools python27-python-pip yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel wget unzip ansible nano @@ -36,22 +38,26 @@ yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel w source /opt/rh/python27/enable # We're now defaulted to 2.7 -pip -q install --upgrade pip # upgrade pip itself -pip -q install pycrypto # python-devel needed to prevent setup.py crash, pycrypto 2.7.1 needed for latest security patch +# Upgrade pip itself +pip -q install --upgrade pip +# # python-devel needed to prevent setup.py crash, pycrypto 2.7.1 needed for latest security patch +pip -q install pycrypto pip -q install setuptools --upgrade wget -q https://github.com/trailofbits/algo/archive/master.zip unzip master.zip cd algo-master || echo "No Algo directory found" -# Must be run from algo-master dir: -pip -q install -r requirements.txt # install Algo local (pusher) dependencies +# Install the local Algo dependencies (must be run from algo-master) +pip -q install -r requirements.txt +# Edit the userlist and any other settings you desire nano config.cfg +# Now you can run the Algo installer! ./algo ``` -## Post-install OSX: +## Post-install OSX * Copy ./configs/*mobileconfig to your local Mac * Install the VPN profile on your Mac (10.10+ required) From 977fbd471bcd27ad5d1adbfedc83b2436014a7f1 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 20 Dec 2016 20:49:13 -0500 Subject: [PATCH 224/769] cleared instructions in the readme --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 69e11398..f0648e8a 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,11 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual 1. Install the dependencies for your operating system: OS X: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` + Linux (deb-based): `sudo easy_install pip && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` + Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) + 2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 3. Start the deploy and follow the instructions: `./algo` @@ -53,16 +56,16 @@ You need to install the [StrongSwan VPN Client for Android 4 and newer](https:// ### Windows -Import your user certificate to your Personal certificate store and your CA certificate to the Local Machine Trusted Root certificate store. Then, add an IKEv2 connection in the network settings and activate additional ciphers for it via Powershell (change the ConnectionName to the name of your IKEv2 connection): +Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. + +If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: `Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA25612 8 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` -Note that an all-in-one Powershell script that imports your personal certificate, sets up the VPN connection, and activates the stronger ciphers for it is included in the `configs` folder. +### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu, etc.) -### StrongSwan Clients (e.g., OpenWRT) - -Find the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files and copy them to your client device. These may be useful if you plan to set up a point-to-point VPN with OpenWRT or other custom device. +Install strongSwan, then copy the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These may require some customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. ### Other Devices From a9dd0af3fe0b5a70ed74a7be204d616df9574023 Mon Sep 17 00:00:00 2001 From: Defunct Date: Wed, 21 Dec 2016 05:55:11 +0000 Subject: [PATCH 225/769] resolves #176 + other ec2 env issues --- roles/cloud-ec2/tasks/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 41f46bc1..343470b1 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -25,8 +25,8 @@ - name: Configure EC2 virtual private clouds ec2_vpc: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" state: present resource_tags: { "Environment":"Algo" } region: "{{ region }}" @@ -39,6 +39,8 @@ - name: Set up Public Subnets Route Table ec2_vpc_route_table: + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" vpc_id: "{{ vpc.vpc_id }}" region: "{{ region }}" state: present From 9c7a6f65d5d0223000c40a825a4d685b69dc8c4e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 22 Dec 2016 21:22:25 +0300 Subject: [PATCH 226/769] Increase timeouts #178 --- ansible.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible.cfg b/ansible.cfg index 03037011..e7173fa8 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -3,6 +3,7 @@ inventory = inventory pipelining = True retry_files_enabled = False host_key_checking = False +timeout = 30 [paramiko_connection] record_host_keys = False From 7159f89c1715805914f4e6758dd7d6bf00e2ccc8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 22 Dec 2016 21:23:00 +0300 Subject: [PATCH 227/769] modify readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0648e8a..e6d437ac 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual OS X: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` - Linux (deb-based): `sudo easy_install pip && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo easy_install pip && sudo apt-get update && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) From 09c3a1399eaa92dfb86a7a80d10bc5d326d54094 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 23 Dec 2016 17:47:04 +0100 Subject: [PATCH 228/769] rewrite and reorder some of the initial setup questions --- algo | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/algo b/algo index 0c4f0666..a9f1b0f6 100755 --- a/algo +++ b/algo @@ -5,11 +5,26 @@ set -e SKIP_TAGS="_null" additional_roles () { + read -p " -Do you want to apply security enhancements? -[y/N]: " -r security_enabled -security_enabled=${security_enabled:-n} -if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi +Do you want to enable VPN Always-On when connected to the cellular network? +[y/N]: " -r OnDemandEnabled_Cellular +OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} +if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi + +read -p " +Do you want to enable VPN Always-On when connected to Wi-Fi? +[y/N]: " -r OnDemandEnabled_WIFI +OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} +if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi + +if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then + read -p " +Do you want to exclude trusted Wi-Fi networks from using the VPN? (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) +: " -r OnDemandEnabled_WIFI_ECXLUDE + OnDemandEnabled_WIFI_ECXLUDE=${OnDemandEnabled_WIFI_ECXLUDE:-_null} + EXTRA_VARS+=" OnDemandEnabled_WIFI_ECXLUDE=$OnDemandEnabled_WIFI_ECXLUDE" +fi read -p " Do you want to install a local DNS resolver to block ads while surfing? @@ -17,12 +32,6 @@ Do you want to install a local DNS resolver to block ads while surfing? dns_enabled=${dns_enabled:-n} if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi -read -p " -Do you want to use auditd for security monitoring (see config.cfg)? -[y/N]: " -r logging_enabled -logging_enabled=${logging_enabled:-n} -if [[ "$logging_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" logging"; fi - read -p " Do you want each user to have their own account for SSH tunneling? [y/N]: " -r ssh_tunneling_enabled @@ -30,27 +39,19 @@ ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi read -p " -Do you want to enable VPN always when connected to Wi-Fi? -[y/N]: " -r OnDemandEnabled_WIFI -OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi - -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then - read -p " -Do you want to exclude trust Wi-Fi networks from VPN usage? (eg: Your home network. Comma-separated value, eg: HomeMeganet,OfficeSuperWifi,AlgoWiFi) -: " -r OnDemandEnabled_WIFI_ECXLUDE - OnDemandEnabled_WIFI_ECXLUDE=${OnDemandEnabled_WIFI_ECXLUDE:-_null} - EXTRA_VARS+=" OnDemandEnabled_WIFI_ECXLUDE=$OnDemandEnabled_WIFI_ECXLUDE" -fi +Do you want to apply operating system security enhancements on the server? +[y/N]: " -r security_enabled +security_enabled=${security_enabled:-n} +if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi read -p " -Do you want to enable VPN always when connected to the cellular network? -[y/N]: " -r OnDemandEnabled_Cellular -OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} -if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi +Do you want to use auditd for security monitoring? (requires configurationg in config.cfg) +[y/N]: " -r logging_enabled +logging_enabled=${logging_enabled:-n} +if [[ "$logging_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" logging"; fi read -p " -Do you want to enable VPN for Windows 10 clients? (Will use insecure algorithms and ciphers) +Do you want the VPN to support Windows 10 clients? (requires RSA certificates and key exchange, less secure) [y/N]: " -r Win10_Enabled Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi From 3d59e27a797cbed9d3a0b81576e1102921885954 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 23 Dec 2016 17:52:29 +0100 Subject: [PATCH 229/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6d437ac..3a463da9 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ### Has Algo been audited? -No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. +No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). ### Why aren't you using Tor? From 9676a23c014c66cd9ed8c5e2b13062773f887d9f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 24 Dec 2016 22:15:02 +0100 Subject: [PATCH 230/769] Add sweet32 info to OpenVPN FAQ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a463da9..3203d72e 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ### Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to past [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? From b444398fab2791f5fc3cbb5456bfe2677ee01f5e Mon Sep 17 00:00:00 2001 From: Damian Gerow Date: Tue, 27 Dec 2016 12:08:54 +0000 Subject: [PATCH 231/769] Drop the MSS for GCE instances --- algo | 2 +- roles/vpn/templates/rules.v4.j2 | 10 ++++++++++ roles/vpn/templates/rules.v6.j2 | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/algo b/algo index a9f1b0f6..19488963 100755 --- a/algo +++ b/algo @@ -286,7 +286,7 @@ Please choose the number of your zone. Press enter for default (#8) zone. esac ROLES="gce vpn cloud" - EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone" + EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1348" } non_cloud () { diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 2a4bb5de..77fa27b3 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -1,3 +1,13 @@ +*mangle +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +{% if max_mss is defined %} +-A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% endif %} +COMMIT *nat :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index c70dc327..fffd3668 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -1,3 +1,13 @@ +*mangle +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +{% if max_mss is defined %} +-A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% endif %} +COMMIT *nat :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] From 0e3d19b5092ac8e40ac7b11cc363ec3da51c6628 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 29 Dec 2016 14:03:47 +0100 Subject: [PATCH 232/769] typo --- algo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo b/algo index a9f1b0f6..f910acdb 100755 --- a/algo +++ b/algo @@ -7,7 +7,7 @@ SKIP_TAGS="_null" additional_roles () { read -p " -Do you want to enable VPN Always-On when connected to the cellular network? +Do you want to enable VPN Always-On when connected to cellular networks? [y/N]: " -r OnDemandEnabled_Cellular OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi @@ -45,7 +45,7 @@ security_enabled=${security_enabled:-n} if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi read -p " -Do you want to use auditd for security monitoring? (requires configurationg in config.cfg) +Do you want to use auditd for security monitoring? (requires configuration in config.cfg) [y/N]: " -r logging_enabled logging_enabled=${logging_enabled:-n} if [[ "$logging_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" logging"; fi From 76de7153feb5c11a00dfc70d3ad620702540988c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 29 Dec 2016 14:03:55 +0100 Subject: [PATCH 233/769] consistency --- docs/ROLES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ROLES.md b/docs/ROLES.md index 8e1df28b..fac4a321 100644 --- a/docs/ROLES.md +++ b/docs/ROLES.md @@ -13,7 +13,7 @@ ## Optional Roles -* **Security Enhancements (Reccommended)** +* **Security Enhancements** * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied * Modify features like core dumps, kernel parameters, and SUID binaries to limit possible attacks * Enhances SSH with modern ciphers and seccomp, and restricts access to older, unwanted features like X11 forwarding and SFTP From 88dca4580a8796b59eea245bb52e458aaad4747b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 29 Dec 2016 14:04:01 +0100 Subject: [PATCH 234/769] clarification --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3203d72e..1517a6dd 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Certificates and configuration files that users will need are placed in the `con ### Apple Devices -Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop (or other secure means). Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices and installing a profile will fully configure the VPN. +Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. ### Android Devices From 208e20ed8e61f73a6e5c694d1b7c1caab7b8945f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 30 Dec 2016 17:14:32 +0100 Subject: [PATCH 235/769] Initial troubleshooting section added closes #187 --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 1517a6dd..f47849e6 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,52 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi ### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). + +## Troubleshooting + +### Error: "You have not agreed to the Xcode license agreements" + +On macOS, did you try to install the dependencies with pip and encounter the following error? + +``` +Downloading cffi-1.9.1.tar.gz (407kB): 407kB downloaded + Running setup.py (path:/private/tmp/pip_build_root/cffi/setup.py) egg_info for package cffi + +You have not agreed to the Xcode license agreements, please run 'xcodebuild -license' (for user-level acceptance) or 'sudo xcodebuild -license' (for system-wide acceptance) from within a Terminal window to review and agree to the Xcode license agreements. + + No working compiler found, or bogus compiler options + passed to the compiler from Python's distutils module. + See the error messages above. + (If they are about -mno-fused-madd and you are on OS/X 10.8, + see http://stackoverflow.com/questions/22313407/ .) + +---------------------------------------- +Cleaning up... +Command python setup.py egg_info failed with error code 1 in /private/tmp/pip_build_root/cffi +Storing debug log for failure in /Users/algore/Library/Logs/pip.log +``` + +The Xcode compiler is installed but requires you to accept its license agreement prior to using it. Run `xcodebuild -license` to agree and then retry installing the dependencies. + +### Error: "fatal error: 'openssl/opensslv.h' file not found" + +On macOS, did you try to install pycrypto and encounter the following error? + +``` +build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found + +#include + + ^ + +1 error generated. + +error: command 'cc' failed with exit status 1 + +---------------------------------------- +Cleaning up... +Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/pip_build_root/cryptography/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-sREEE5-record/install-record.txt --single-version-externally-managed --compile failed with error code 1 in /private/tmp/pip_build_root/cryptography +Storing debug log for failure in /Users/algore/Library/Logs/pip.log +``` + +You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. \ No newline at end of file From 4dc476572b71477a770d20e9714eb57d542e4100 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 30 Dec 2016 19:20:09 +0100 Subject: [PATCH 236/769] clarifications --- README.md | 6 +++--- docs/ROLES.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f47849e6..a0f62106 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to ### Android Devices -You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. It's very simple to configure the StrongSwan VPN Client, just make a new profile with the IP address of your VPN server and choose which certificate to use. +You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. It's very simple to configure the StrongSwan VPN Client, just make a new profile with the IP address of your VPN server and choose which certificate to use. ### Windows @@ -82,9 +82,9 @@ Depending on the platform, you may need one or multiple of the following files. ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access and their tunneling options are limited (`ssh -N` is required). This is done to ensure that users have the least access required to tunnel through the server. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and have limited tunneling options (e.g., `ssh -N` is required). This is done to ensure that users have the least access required to tunnel through the server and can perform no other actions. -Use the command below to start an SSH tunnel, replacing `ip` and `user` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through Algo. +Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` diff --git a/docs/ROLES.md b/docs/ROLES.md index fac4a321..1f438c3d 100644 --- a/docs/ROLES.md +++ b/docs/ROLES.md @@ -16,12 +16,12 @@ * **Security Enhancements** * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied * Modify features like core dumps, kernel parameters, and SUID binaries to limit possible attacks - * Enhances SSH with modern ciphers and seccomp, and restricts access to older, unwanted features like X11 forwarding and SFTP -* **Ad Blocking and Compression HTTP Proxy** + * Enhances SSH with modern ciphers and seccomp, and restricts access to old or unwanted features like X11 forwarding and SFTP +* **Proxy-based Adblocking and Compression** * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations -* **DNS Ad Blocking** +* **DNS-based Adblocking** * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations * **Security Monitoring and Logging** From f6f81aab8ca0c5c66db46eed21954e5863e246ed Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 30 Dec 2016 23:52:18 +0100 Subject: [PATCH 237/769] Add setup clarification and Little Snitch FAQ Closes #134 Closes #188 --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a0f62106..db83b7d8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw * Blocks ads with a local DNS resolver and HTTP proxy (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Google Cloud Engine, Microsoft Azure or your own server +* Installs to DigitalOcean, Amazon EC2, Google Compute Engine, Microsoft Azure or your own server ## Anti-features @@ -27,16 +27,17 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw The easiest way to get an Algo server running is to let it setup a _new_ virtual machine in the cloud for you. -1. Install the dependencies for your operating system: +1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). +2. Install the dependencies for your operating system: - OS X: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` + macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` Linux (deb-based): `sudo easy_install pip && sudo apt-get update && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) -2. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -3. Start the deploy and follow the instructions: `./algo` +3. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +4. Start the deploy and follow the instructions by running: `./algo`. There are several optional features available. None are required for a fully functional VPN server. These features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You now have an Algo VPN server on the internet. @@ -170,4 +171,8 @@ Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/p Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. \ No newline at end of file +You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. + +### Little Snitch is broken when connected to the VPN + +Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). From 969de00ae817eb5fc6e2aaf7b402e7251e6aeb02 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 31 Dec 2016 00:27:36 +0100 Subject: [PATCH 238/769] Add link to development plan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db83b7d8..f959fe94 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ### Has Algo been audited? -No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). +No. This project is under [active development](https://github.com/trailofbits/algo/projects/1). We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). ### Why aren't you using Tor? From 23d8a0603962760c55e8a97e91b4a35076760ebb Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 31 Dec 2016 03:02:32 +0100 Subject: [PATCH 239/769] temporarily disabling the auditd role I'm not sure this role ever worked as intended. Let's just pretend it doesn't exist until we rewrite it with go-audit in #16 --- algo | 6 ------ 1 file changed, 6 deletions(-) diff --git a/algo b/algo index 22f181a4..5d49776d 100755 --- a/algo +++ b/algo @@ -44,12 +44,6 @@ Do you want to apply operating system security enhancements on the server? security_enabled=${security_enabled:-n} if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi -read -p " -Do you want to use auditd for security monitoring? (requires configuration in config.cfg) -[y/N]: " -r logging_enabled -logging_enabled=${logging_enabled:-n} -if [[ "$logging_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" logging"; fi - read -p " Do you want the VPN to support Windows 10 clients? (requires RSA certificates and key exchange, less secure) [y/N]: " -r Win10_Enabled From 9975cecbb3a7ba0846d85ec871c32258e8e758d6 Mon Sep 17 00:00:00 2001 From: Glenn Rempe Date: Fri, 30 Dec 2016 18:40:44 -0800 Subject: [PATCH 240/769] Fixes #197, remove unused do_ssh_name var from examples and code --- algo | 2 +- docs/ADVANCED.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/algo b/algo index 5d49776d..4dc09eef 100755 --- a/algo +++ b/algo @@ -174,7 +174,7 @@ Enter the number of your desired region: esac ROLES="digitalocean vpn cloud" -EXTRA_VARS="do_access_token=$do_access_token do_ssh_name=$do_ssh_name do_server_name=$do_server_name do_region=$do_region" +EXTRA_VARS="do_access_token=$do_access_token do_server_name=$do_server_name do_region=$do_region" } ec2 () { diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index ad057e8e..99366502 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -35,7 +35,7 @@ cd algo && ./algo Example for DigitalOcean: ``` -ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=my_secret_token do_ssh_name=my_ssh_key do_server_name=algo.local do_region=ams2' +ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' ``` ### Roles @@ -60,7 +60,6 @@ Server roles: Required variables: - do_access_token -- do_ssh_name - do_server_name - do_region From 9a46b671f76624156ac2a628957ae002256aaa6f Mon Sep 17 00:00:00 2001 From: Glenn Rempe Date: Fri, 30 Dec 2016 18:47:02 -0800 Subject: [PATCH 241/769] Fixes #198, replace typo ECXLUDE with EXCLUDE --- algo | 6 +++--- roles/vpn/templates/mobileconfig.j2 | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/algo b/algo index 5d49776d..9d6a84a6 100755 --- a/algo +++ b/algo @@ -21,9 +21,9 @@ if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_ if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then read -p " Do you want to exclude trusted Wi-Fi networks from using the VPN? (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) -: " -r OnDemandEnabled_WIFI_ECXLUDE - OnDemandEnabled_WIFI_ECXLUDE=${OnDemandEnabled_WIFI_ECXLUDE:-_null} - EXTRA_VARS+=" OnDemandEnabled_WIFI_ECXLUDE=$OnDemandEnabled_WIFI_ECXLUDE" +: " -r OnDemandEnabled_WIFI_EXCLUDE + OnDemandEnabled_WIFI_EXCLUDE=${OnDemandEnabled_WIFI_EXCLUDE:-_null} + EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=$OnDemandEnabled_WIFI_EXCLUDE" fi read -p " diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index c118e5f1..d2873260 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -12,8 +12,8 @@ 1 OnDemandRules -{% if OnDemandEnabled_WIFI_ECXLUDE is defined and OnDemandEnabled_WIFI_ECXLUDE != '_null' %} -{% set WIFI_ECXLUDE_LIST = OnDemandEnabled_WIFI_ECXLUDE.split(',') %} +{% if OnDemandEnabled_WIFI_EXCLUDE is defined and OnDemandEnabled_WIFI_EXCLUDE != '_null' %} +{% set WIFI_EXCLUDE_LIST = OnDemandEnabled_WIFI_EXCLUDE.split(',') %} Action Disconnect @@ -21,7 +21,7 @@ WiFi SSIDMatch -{% for network_name in WIFI_ECXLUDE_LIST %} +{% for network_name in WIFI_EXCLUDE_LIST %} {{ network_name }} {% endfor %} From 0f7af34e63be8d1554e28966c5e9849085bf350f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 31 Dec 2016 16:47:39 +0100 Subject: [PATCH 242/769] clarify setup steps --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f959fe94..f1cb6ff2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw * Blocks ads with a local DNS resolver and HTTP proxy (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Google Compute Engine, Microsoft Azure or your own server +* Installs to DigitalOcean, Amazon EC2, Google Compute Engine, Microsoft Azure, or your own server ## Anti-features @@ -28,7 +28,7 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw The easiest way to get an Algo server running is to let it setup a _new_ virtual machine in the cloud for you. 1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. Install the dependencies for your operating system: +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and install the dependencies for your operating system. Open a terminal and `cd` into the directory where you have Algo, then: macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` @@ -36,7 +36,7 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) -3. Open the file `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +3. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 4. Start the deploy and follow the instructions by running: `./algo`. There are several optional features available. None are required for a fully functional VPN server. These features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You now have an Algo VPN server on the internet. @@ -45,7 +45,7 @@ Note: for local or scripted deployment instructions see the [Advanced Usage](/do ## Configure the VPN Clients -Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of the Algo VPN server. +Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of your new Algo VPN server. ### Apple Devices @@ -83,7 +83,7 @@ Depending on the platform, you may need one or multiple of the following files. ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and have limited tunneling options (e.g., `ssh -N` is required). This is done to ensure that users have the least access required to tunnel through the server and can perform no other actions. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and only have limited tunneling options (e.g., `ssh -N` is required). This is done to ensure that SSH users have the least access required to tunnel through the server and can perform no other actions. Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. From 3d5296e3a32392e0fdb95358860a64616ba99db1 Mon Sep 17 00:00:00 2001 From: Danny Rogers Date: Sat, 31 Dec 2016 14:09:48 -0500 Subject: [PATCH 243/769] Added eu-west-2 (London) region support --- algo | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/algo b/algo index 0ff9178b..ced10074 100755 --- a/algo +++ b/algo @@ -206,10 +206,11 @@ Name the vpn server: 7. ap-southeast-1 Asia Pacific (Singapore) 8. ap-southeast-2 Asia Pacific (Sydney) 9. ap-northeast-1 Asia Pacific (Tokyo) - 10. eu-central-1 EU (Frankfurt) + 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) - 12. sa-east-1 South America (São Paulo) - 13. ca-central-1 Canada (Central) + 12. eu-west-2 EU (London) + 13. sa-east-1 South America (São Paulo) + 14. ca-central-1 Canada (Central) Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} @@ -226,8 +227,9 @@ Enter the number of your desired region: 9) region="ap-northeast-1" ;; 10) region="eu-central-1" ;; 11) region="eu-west-1" ;; - 12) region="sa-east-1" ;; - 13) region="ca-central-1" ;; + 12) region="eu-west-2";; + 13) region="sa-east-1" ;; + 14) region="ca-central-1" ;; esac ROLES="ec2 vpn cloud" From 460ff57f9bf171d4f65765a41c8cb9d7286dbad5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 1 Jan 2017 15:18:53 +0100 Subject: [PATCH 244/769] Update ADVANCED.md Pulled in some changes from #199, thanks @grempe --- docs/ADVANCED.md | 107 +++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 99366502..f4a7ede8 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -1,25 +1,10 @@ # Advanced Usage -## Requirements - -Before you begin, make sure you have installed all the dependencies necessary for your use case. Algo depends on the software below and most of it will be installed via the `requirements.txt` file. - -* ansible >= 2.1 -* python >= 2.6 -* [dopy=0.3.5](https://github.com/Wiredcraft/dopy) -* [boto](https://github.com/boto/boto) -* [azure >= 0.7.1](https://github.com/Azure/azure-sdk-for-python) -* [apache-libcloud](https://github.com/apache/libcloud) -* [libcloud](https://curl.haxx.se/docs/caextract.html) (for Mac OS) -* [six](https://github.com/JioCloud/python-six) -* SHell or BASH -* libselinux-python (for RedHat based distros) +Make sure you have installed all the dependencies necessary for your operating system as described in the README. ## Local Deployment -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwite the rules, just skip the `iptables` tag. You can find some information about tags below. - -It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It is easier to use apt, however, Ubuntu 16.04 only comes with ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. +It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install Ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. tl;dr: @@ -30,9 +15,17 @@ git clone https://github.com/trailofbits/algo cd algo && ./algo ``` +**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. + ## Scripted Deployment -Example for DigitalOcean: +You can deploy Algo non-interactively by running the Ansible playbooks directly with `ansible-playbook`. + +`ansible-playbook` accepts "tags" via the `-t` or `TAGS` options. You can pass tags as a list of comma separated values. Ansible will only run plays (install roles) with the specified tags. + +`ansible-playbook` accepts variables via the `-e` or `--extra-vars` option. You can pass variables as space separated key=value pairs. Algo requires certain variables that are listed below. + +Here is a full example for DigitalOcean: ``` ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' @@ -48,13 +41,26 @@ Cloud roles: Server roles: -- role: vpn, tags: vpn +- role: vpn, tags: vpn - role: dns_adblocking, tags: dns, adblock - role: proxy, tags: proxy, adblock - role: logging, tags: logging - role: security, tags: security - role: ssh_tunneling, tags: ssh_tunneling +Note: The `vpn` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: + +- OnDemandEnabled_WIFI=Y +- OnDemandEnabled_Cellular=Y + +### Local Installation + +Required variables: + +- server_ip +- server_user +- IP_subject_alt_name + ### Digital Ocean Required variables: @@ -63,7 +69,7 @@ Required variables: - do_server_name - do_region -Possible regions: +Possible options for `do_region`: - ams2 - ams3 @@ -78,6 +84,32 @@ Possible regions: - tor1 - blr1 +### Amazon EC2 + +Required variables: + +- aws_access_key +- aws_secret_key +- aws_server_name +- ssh_public_key +- region + +Possible options for `region`: + +- us-east-1 +- us-east-2 +- us-west-1 +- us-west-2 +- ap-south-1 +- ap-northeast-2 +- ap-southeast-1 +- ap-southeast-2 +- ap-northeast-1 +- eu-central-1 +- eu-west-1 +- eu-west-2 +- sa-east-1 + ### Google Cloud Engine Required variables: @@ -87,7 +119,7 @@ Required variables: - ssh_public_key - zone -Possible zones: +Possible options for `zone`: - us-central1-a - us-central1-b @@ -102,36 +134,3 @@ Possible zones: - asia-east1-a - asia-east1-b - asia-east1-c - -### Amazon EC2 - -Required variables: - -- aws_access_key -- aws_secret_key -- aws_server_name -- ssh_public_key -- region - -Possible regions: - -- us-east-1 -- us-east-2 -- us-west-1 -- us-west-2 -- ap-south-1 -- ap-northeast-2 -- ap-southeast-1 -- ap-southeast-2 -- ap-northeast-1 -- eu-central-1 -- eu-west-1 -- sa-east-1 - -### Local Installation - -Required variables: - -- server_ip -- server_user -- IP_subject_alt_name From 13b403d4b9182067b322dc01fc8ba128f19e1844 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 1 Jan 2017 22:57:08 +0100 Subject: [PATCH 245/769] Update ADVANCED.md --- docs/ADVANCED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index f4a7ede8..961c4493 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -110,7 +110,7 @@ Possible options for `region`: - eu-west-2 - sa-east-1 -### Google Cloud Engine +### Google Compute Engine Required variables: From b27d8de484928c2ca0320cb29d2164021308fa04 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 4 Jan 2017 02:23:51 +0100 Subject: [PATCH 246/769] Closes #82 --- README.md | 2 ++ logo.png | Bin 0 -> 58315 bytes 2 files changed, 2 insertions(+) create mode 100644 logo.png diff --git a/README.md b/README.md index f1cb6ff2..27fcf854 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![AlgoVPN Logo](logo.png) + # Algo VPN [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c33787bd174e93bef28dee0834afcc13ad3ec2aa GIT binary patch literal 58315 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�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 literal 0 HcmV?d00001 From 71c5cd6cc25536510993e872d9e63a2eb9f642c9 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 4 Jan 2017 02:26:15 +0100 Subject: [PATCH 247/769] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 27fcf854..f1cb6ff2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -![AlgoVPN Logo](logo.png) - # Algo VPN [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) From ce2e9f17d7fe7df963ec2153b5dede0c7d8bb1e0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 4 Jan 2017 17:16:55 +0300 Subject: [PATCH 248/769] update ADVANCED.md #199 --- docs/ADVANCED.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 961c4493..174ee3bd 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -28,11 +28,15 @@ You can deploy Algo non-interactively by running the Ansible playbooks directly Here is a full example for DigitalOcean: ``` -ansible-playbook deploy.yml -t digitalocean,vpn -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' +ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' ``` ### Roles +Required tags: + +- cloud + Cloud roles: - role: cloud-digitalocean, tags: digitalocean @@ -51,10 +55,15 @@ Server roles: Note: The `vpn` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: - OnDemandEnabled_WIFI=Y +- OnDemandEnabled_WIFI_EXCLUDE=HomeNet - OnDemandEnabled_Cellular=Y ### Local Installation +Required tags: + +- local + Required variables: - server_ip From f2461652987b1517dafe947c9302a6e4a51d0117 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 4 Jan 2017 17:45:42 +0300 Subject: [PATCH 249/769] Fix a typo --- roles/vpn/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 8bbb4416..08971cac 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -127,7 +127,7 @@ - name: Build the server pair shell: > - ./easyrsa gen-req {{ IP_subject_alt_name }} batch nopass -- -passin pass:qwe1 -subj "/CN={{ IP_subject_alt_name }}" && + ./easyrsa gen-req {{ IP_subject_alt_name }} batch nopass -- -subj "/CN={{ IP_subject_alt_name }}" && ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' sign-req server {{ IP_subject_alt_name }} -- -passin pass:"{{ easyrsa_CA_password }}" && touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' args: From 3c83736d2e0dff18e7ee822f52e852761ee8bf50 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 5 Jan 2017 10:16:24 +0300 Subject: [PATCH 250/769] allow spaces in WIFI-SSIDs --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index ced10074..37e4a5f6 100755 --- a/algo +++ b/algo @@ -23,7 +23,7 @@ if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then Do you want to exclude trusted Wi-Fi networks from using the VPN? (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) : " -r OnDemandEnabled_WIFI_EXCLUDE OnDemandEnabled_WIFI_EXCLUDE=${OnDemandEnabled_WIFI_EXCLUDE:-_null} - EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=$OnDemandEnabled_WIFI_EXCLUDE" + EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=\"$OnDemandEnabled_WIFI_EXCLUDE\"" fi read -p " From 1a813721928e9db56515afde38e3d7a9bac8df00 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 30 Nov 2016 12:23:24 +0300 Subject: [PATCH 251/769] EC2 Encryption Implemented #133 --- requirements.txt | 4 +- roles/cloud-ec2/tasks/encrypt_image.yml | 72 +++++++++++++++++++++++++ roles/cloud-ec2/tasks/main.yml | 8 +-- 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 roles/cloud-ec2/tasks/encrypt_image.yml diff --git a/requirements.txt b/requirements.txt index 2aa7e050..99ffb2aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ ansible>=2.1 dopy==0.3.5 -boto -azure==2.0.0rc5 +boto>=2.5 +azure>=2.0.0rc5 apache-libcloud six pyopenssl diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml new file mode 100644 index 00000000..ce4406f1 --- /dev/null +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -0,0 +1,72 @@ +- name: Locate official Ubuntu 16.04 AMI for region + ec2_ami_find: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" + owner: 099720109477 + sort: name + sort_order: descending + sort_end: 1 + region: "{{ region }}" + register: ami_search + +- set_fact: + source_ami_image: "{{ ami_search.results[0].ami_id }}" + +# +# https://github.com/ansible/ansible-modules-extras/issues/3565 +# +#- name: Copy to an encrypted image + #ec2_ami_copy: + #aws_access_key: "{{ aws_access_key }}" + #aws_secret_key: "{{ aws_secret_key }}" + #description: ENC_IMAGE + #encrypted: yes + #name: newimage + #region: "{{ region }}" + #source_image_id: "{{ source_ami_image }}" + #source_region: "{{ region }}" + #register: ec2_ami_copy + #when: ami_encrypted_tag is not defined or (ami_encrypted_tag is defined and ami_encrypted_tag != true) +#- debug: var=ec2_ami_copy + +# +# https://github.com/ansible/ansible-modules-extras/issues/3565 +# +- name: Copy to an encrypted image + shell: > + aws ec2 copy-image --source-region '{{ region }}' --region '{{ region }}' --encrypted --source-image-id '{{ source_ami_image }}' --name 'ubuntu-xenial-16.04-amd64-server-encrypted' + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: ec2_ami_copy + +- set_fact: + ami_image_ouput: "{{ ec2_ami_copy.stdout|from_json }}" + +- set_fact: + ami_encrypted_image: "{{ ami_image_ouput['ImageId'] }}" + +- name: Add tags to the encrypted image + ec2_tag: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + region: "{{ region }}" + resource: "{{ ami_encrypted_image }}" + state: present + tags: + Name: "ubuntu-xenial-16.04-amd64-server-encrypted" + Encrypted: "true" + +- name: Confirm the encrypted image + ec2_ami_find: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + ami_id: "{{ ami_encrypted_image }}" + region: "{{ region }}" + owner: self + state: available + register: ec2_ami_find_encrypted + until: ec2_ami_find_encrypted.results|length > 0 + retries: 60 + delay: 10 diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 343470b1..6c49a98e 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,7 +1,7 @@ - name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" owner: 099720109477 sort: creationDate @@ -10,8 +10,8 @@ region: "{{ region }}" register: ami_search -- set_fact: - ami_image: "{{ ami_search.results[0].ami_id }}" +- include: encrypt_image.yml + when: ami_encrypted_tag is not defined or (ami_encrypted_tag is defined and ami_encrypted_tag != "true1") - name: Add ssh public key ec2_key: From 0eb048383a3d89b893380f4d7956c31a2cef9b87 Mon Sep 17 00:00:00 2001 From: Defunct Date: Sat, 10 Dec 2016 03:22:16 +0000 Subject: [PATCH 252/769] refactored ec2 encryption --- roles/cloud-ec2/tasks/encrypt_image.yml | 99 ++++++++----------------- roles/cloud-ec2/tasks/main.yml | 6 +- 2 files changed, 34 insertions(+), 71 deletions(-) diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml index ce4406f1..4590332e 100644 --- a/roles/cloud-ec2/tasks/encrypt_image.yml +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -1,72 +1,35 @@ -- name: Locate official Ubuntu 16.04 AMI for region +- name: Check if the encrypted image already exist ec2_ami_find: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" - owner: 099720109477 - sort: name + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + owner: self + sort: creationDate sort_order: descending sort_end: 1 - region: "{{ region }}" - register: ami_search - -- set_fact: - source_ami_image: "{{ ami_search.results[0].ami_id }}" - -# -# https://github.com/ansible/ansible-modules-extras/issues/3565 -# -#- name: Copy to an encrypted image - #ec2_ami_copy: - #aws_access_key: "{{ aws_access_key }}" - #aws_secret_key: "{{ aws_secret_key }}" - #description: ENC_IMAGE - #encrypted: yes - #name: newimage - #region: "{{ region }}" - #source_image_id: "{{ source_ami_image }}" - #source_region: "{{ region }}" - #register: ec2_ami_copy - #when: ami_encrypted_tag is not defined or (ami_encrypted_tag is defined and ami_encrypted_tag != true) -#- debug: var=ec2_ami_copy - -# -# https://github.com/ansible/ansible-modules-extras/issues/3565 -# -- name: Copy to an encrypted image - shell: > - aws ec2 copy-image --source-region '{{ region }}' --region '{{ region }}' --encrypted --source-image-id '{{ source_ami_image }}' --name 'ubuntu-xenial-16.04-amd64-server-encrypted' - environment: - AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" - AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" - register: ec2_ami_copy - -- set_fact: - ami_image_ouput: "{{ ec2_ami_copy.stdout|from_json }}" - -- set_fact: - ami_encrypted_image: "{{ ami_image_ouput['ImageId'] }}" - -- name: Add tags to the encrypted image - ec2_tag: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - region: "{{ region }}" - resource: "{{ ami_encrypted_image }}" - state: present - tags: - Name: "ubuntu-xenial-16.04-amd64-server-encrypted" - Encrypted: "true" - -- name: Confirm the encrypted image - ec2_ami_find: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - ami_id: "{{ ami_encrypted_image }}" - region: "{{ region }}" - owner: self state: available - register: ec2_ami_find_encrypted - until: ec2_ami_find_encrypted.results|length > 0 - retries: 60 - delay: 10 + ami_tags: + Algo: "encrypted" + region: "{{ region }}" + register: search_crypt + +- set_fact: + enc_image: "{{ search_crypt.results[0].image_id }}" + when: search_crypt.results + +- name: Copy to an encrypted image + ec2_ami_copy: + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + encrypted: yes + name: algo + region: "{{ region }}" + source_image_id: "{{ image_id }}" + source_region: "{{ region }}" + tags: + Algo: "encrypted" + wait: true + register: enc_image + when: enc_image is not defined + +- set_fact: + image_id: "{{ enc_image.image_id }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 6c49a98e..886fd144 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,7 +1,7 @@ - name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" owner: 099720109477 sort: creationDate @@ -11,7 +11,7 @@ register: ami_search - include: encrypt_image.yml - when: ami_encrypted_tag is not defined or (ami_encrypted_tag is defined and ami_encrypted_tag != "true1") + when: encrypted is defined - name: Add ssh public key ec2_key: From b0f9ab94b13639af43b673f3e2841c66edba294c Mon Sep 17 00:00:00 2001 From: Defunct Date: Sat, 10 Dec 2016 18:50:49 +0000 Subject: [PATCH 253/769] ec2_ami_copy boto3 module, KMS, tagging, AMI caching (Encrypted support) --- .gitignore | 1 + library/ec2_ami_copy.py | 216 ++++++++++++++++++++++++ roles/cloud-ec2/tasks/encrypt_image.yml | 10 +- roles/cloud-ec2/tasks/main.yml | 5 +- 4 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 library/ec2_ami_copy.py diff --git a/.gitignore b/.gitignore index e1c9fea7..e162478e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.retry +.idea/ configs/* inventory_users *.kate-swp diff --git a/library/ec2_ami_copy.py b/library/ec2_ami_copy.py new file mode 100644 index 00000000..629a48c6 --- /dev/null +++ b/library/ec2_ami_copy.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.1'} + +DOCUMENTATION = ''' +--- +module: ec2_ami_copy +short_description: copies AMI between AWS regions, return new image id +description: + - Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5 +version_added: "2.0" +options: + source_region: + description: + - the source region that AMI should be copied from + required: true + source_image_id: + description: + - the id of the image in source region that should be copied + required: true + name: + description: + - The name of the new image to copy + required: true + default: null + description: + description: + - An optional human-readable string describing the contents and purpose of the new AMI. + required: false + default: null + encrypted: + description: + - Whether or not to encrypt the target image + required: false + default: null + version_added: "2.2" + kms_key_id: + description: + - KMS key id used to encrypt image. If not specified, uses default EBS Customer Master Key (CMK) for your account. + required: false + default: null + version_added: "2.2" + wait: + description: + - wait for the copied AMI to be in state 'available' before returning. + required: false + default: false + tags: + description: + - a hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}' + required: false + default: null + +author: Amir Moulavi , Tim C +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Basic AMI Copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + +# AMI copy wait until available +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + wait: yes + register: image_id + +# Named AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + name: My-Awesome-AMI + description: latest patch + +# Tagged AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + tags: + Name: My-Super-AMI + Patch: 1.2.3 + +# Encrypted AMI copy +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + encrypted: yes + +# Encrypted AMI copy with specified key +- ec2_ami_copy: + source_region: us-east-1 + region: eu-west-1 + source_image_id: ami-xxxxxxx + encrypted: yes + kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, get_aws_connection_info) + +try: + import boto + import boto.ec2 + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError, NoRegionError + HAS_BOTO3 = True +except ImportError: + HAS_BOTO3 = False + + + +def copy_image(ec2, module): + """ + Copies an AMI + + module : AnsibleModule object + ec2: ec2 connection object + """ + + tags = module.params.get('tags') + + params = {'SourceRegion': module.params.get('source_region'), + 'SourceImageId': module.params.get('source_image_id'), + 'Name': module.params.get('name'), + 'Description': module.params.get('description'), + 'Encrypted': module.params.get('encrypted'), +# 'KmsKeyId': module.params.get('kms_key_id') + } + if module.params.get('kms_key_id'): + params['KmsKeyId'] = module.params.get('kms_key_id') + + try: + image_id = ec2.copy_image(**params)['ImageId'] + if module.params.get('wait'): + ec2.get_waiter('image_available').wait(ImageIds=[image_id]) + if module.params.get('tags'): + ec2.create_tags( + Resources=[image_id], + Tags=[{'Key' : k, 'Value': v} for k,v in module.params.get('tags').items()] + ) + + module.exit_json(changed=True, image_id=image_id) + except ClientError as ce: + module.fail_json(msg=ce) + except NoCredentialsError: + module.fail_json(msg="Unable to locate AWS credentials") + except Exception as e: + module.fail_json(msg=str(e)) + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + source_region=dict(required=True), + source_image_id=dict(required=True), + name=dict(required=True), + description=dict(default=''), + encrypted=dict(type='bool', required=False), + kms_key_id=dict(type='str', required=False), + wait=dict(type='bool', default=False, required=False), + tags=dict(type='dict'))) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + # TODO: Check botocore version + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + + if HAS_BOTO3: + + try: + ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, + **aws_connect_params) + except NoRegionError: + module.fail_json(msg='AWS Region is required') + else: + module.fail_json(msg='boto3 required for this module') + + copy_image(ec2, module) + + +if __name__ == '__main__': + main() diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml index 4590332e..da46534a 100644 --- a/roles/cloud-ec2/tasks/encrypt_image.yml +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -13,7 +13,7 @@ register: search_crypt - set_fact: - enc_image: "{{ search_crypt.results[0].image_id }}" + ami_image: "{{ search_crypt.results[0].ami_id }}" when: search_crypt.results - name: Copy to an encrypted image @@ -22,14 +22,16 @@ aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" encrypted: yes name: algo + kms_key_id: "{{ kms_key_id | default(omit) }}" region: "{{ region }}" - source_image_id: "{{ image_id }}" + source_image_id: "{{ ami_image }}" source_region: "{{ region }}" tags: Algo: "encrypted" wait: true register: enc_image - when: enc_image is not defined + when: not search_crypt.results - set_fact: - image_id: "{{ enc_image.image_id }}" + ami_image: "{{ enc_image.image_id }}" + when: not search_crypt.results diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 886fd144..edbfc025 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -10,8 +10,11 @@ region: "{{ region }}" register: ami_search +- set_fact: + ami_image: "{{ ami_search.results[0].ami_id }}" + - include: encrypt_image.yml - when: encrypted is defined + tags: [encrypted] - name: Add ssh public key ec2_key: From bde51fdd1df8487ee709574bfa019fe807b01939 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 9 Jan 2017 22:29:07 +0300 Subject: [PATCH 254/769] Update requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 99ffb2aa..706af7f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ ansible>=2.1 dopy==0.3.5 boto>=2.5 -azure>=2.0.0rc5 +boto3 +azure==2.0.0rc5 apache-libcloud six pyopenssl From 9963dc0cc74449b8f39d34c91c4cc7b7a396079f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 9 Jan 2017 16:25:12 -0500 Subject: [PATCH 255/769] Readability suggestions per Mike Russell --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f1cb6ff2..2e5dc730 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everyw ## Deploy the Algo Server -The easiest way to get an Algo server running is to let it setup a _new_ virtual machine in the cloud for you. +The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and install the dependencies for your operating system. Open a terminal and `cd` into the directory where you have Algo, then: +1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) +3. Install Algo's dependencies for your operating system. To do this, open a terminal and `cd` into the directory where you downloaded Algo, then: macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` @@ -36,8 +37,8 @@ The easiest way to get an Algo server running is to let it setup a _new_ virtual Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) -3. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -4. Start the deploy and follow the instructions by running: `./algo`. There are several optional features available. None are required for a fully functional VPN server. These features are described in greater detail in [ROLES.md](docs/ROLES.md). +4. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +5. Start the deploy. Return to your terminal, run `./algo`, and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You now have an Algo VPN server on the internet. From 1d49ab6dc463cc99980e7319fa31e6b43ce755f8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 9 Jan 2017 21:34:32 -0500 Subject: [PATCH 256/769] more minor changes for readability --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e5dc730..58ad7b64 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) 4. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -5. Start the deploy. Return to your terminal, run `./algo`, and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). +5. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You now have an Algo VPN server on the internet. From a93b0a0f444459a00d3c962ca9215c8726a67ccb Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 10 Jan 2017 18:55:59 +0300 Subject: [PATCH 257/769] skip encrypted by default #133 --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index 37e4a5f6..26fbbd22 100755 --- a/algo +++ b/algo @@ -2,7 +2,7 @@ set -e -SKIP_TAGS="_null" +SKIP_TAGS="_null encrypted" additional_roles () { From 2598d5874688f7720e557f57d4058ed34ccc5fdd Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 10 Jan 2017 19:04:29 +0300 Subject: [PATCH 258/769] Update ADVANCED.md --- docs/ADVANCED.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 174ee3bd..ab1bc1a4 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -119,6 +119,10 @@ Possible options for `region`: - eu-west-2 - sa-east-1 +Additional tags: + +- [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) (enabled by default) + ### Google Compute Engine Required variables: From 88518240fcc0ae8c8fde6270a0d4b771b612d44d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 20:55:07 +0300 Subject: [PATCH 259/769] Fix for the local installation --- config.cfg | 5 +++++ deploy.yml | 3 +++ 2 files changed, 8 insertions(+) diff --git a/config.cfg b/config.cfg index e27c4555..c5cbe0fd 100644 --- a/config.cfg +++ b/config.cfg @@ -77,9 +77,14 @@ congrats: | "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" "# The p12 password is {{ easyrsa_p12_export_password }} #" "# The CA key password is {{ easyrsa_CA_password }} #" + "#----------------------------------------------------------------------#" + +additional_information: | + "#----------------------------------------------------------------------#" "# Shell access: ssh -i {{ ansible_ssh_private_key_file }} {{ ansible_ssh_user }}@{{ ansible_ssh_host }} #" "#----------------------------------------------------------------------#" + SSH_keys: comment: algo@ssh private: configs/algo.pem diff --git a/deploy.yml b/deploy.yml index a94cc49e..649481e3 100644 --- a/deploy.yml +++ b/deploy.yml @@ -60,3 +60,6 @@ post_tasks: - debug: msg="{{ congrats.split('\n') }}" tags: always + + - debug: msg="{{ additional_information.split('\n') }}" + tags: cloud From a50a396b94aa8c5f81677384e737b67f32b11d0a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 20:55:44 +0300 Subject: [PATCH 260/769] addtiional fixes --- roles/common/tasks/main.yml | 1 + roles/vpn/tasks/iptables.yml | 7 +++++++ roles/vpn/tasks/main.yml | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 12d7109c..79c7cfeb 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -60,6 +60,7 @@ - sendmail - iptables-persistent - cgroup-tools + - openssl tags: - always diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml index aeed994b..0088a6d4 100644 --- a/roles/vpn/tasks/iptables.yml +++ b/roles/vpn/tasks/iptables.yml @@ -4,6 +4,13 @@ template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 with_items: - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } + notify: + - restart iptables + +- name: Iptables configured + template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 + when: ipv6_support is defined and ipv6_support == "yes" + with_items: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: - restart iptables diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 08971cac..0ec3a180 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -21,6 +21,7 @@ - /usr/lib/ipsec/stroke notify: - restart apparmor + tags: ['apparmor'] - name: Enable services service: name={{ item }} enabled=yes @@ -38,8 +39,9 @@ - name: Configure ip6tables so IPSec traffic can traverse the tunnel iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - when: (security_enabled is not defined) or - (security_enabled is defined and security_enabled != "y") + when: ((security_enabled is not defined) or + (security_enabled is defined and security_enabled != "y")) and + ipv6_support is defined and ipv6_support == "yes" notify: - save iptables From cbf59addb3581ae4acd178edc2a3e82fdc2f515d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 21:02:41 +0300 Subject: [PATCH 261/769] additional tags --- roles/dns_adblocking/tasks/main.yml | 1 + roles/proxy/tasks/main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index a37bf9cd..e3692bbf 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -14,6 +14,7 @@ - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq + tags: ['apparmor'] - name: Ensure that the dnsmasq service directory exist file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index e1d8b9d3..9117dfb7 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -19,6 +19,7 @@ - name: Enforce the privoxy AppArmor policy shell: aa-enforce usr.sbin.privoxy + tags: ['apparmor'] - name: Ensure that the privoxy service directory exist file: path=/etc/systemd/system/privoxy.service.d/ state=directory mode=0755 owner=root group=root From 0a4e19a6d25b9d57fe1488c71a1f0bf6d9769b69 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 23:20:47 +0300 Subject: [PATCH 262/769] TravisCI initial. Testing all the components except the cloud roles. #154 --- .travis.yml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0d05220f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +--- + +language: python +python: "2.7" +sudo: required +dist: trusty + +matrix: + fast_finish: true + +addons: + apt: + sources: + - sourceline: 'ppa:ubuntu-lxc/stable' + packages: + - python-pip + - lxc + - lxc-templates + - expect-dev + - debootstrap + +cache: + directories: + - $HOME/lxc/ + pip: true + +before_cache: + - mkdir $HOME/lxc + - sudo tar cf $HOME/lxc/cache.tar /var/cache/lxc/ + - sudo chown $USER. $HOME/lxc/cache.tar + +env: + - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial + +install: + - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." + - export LXC_ROOTFS=/var/lib/lxc/$LXC_NAME/rootfs + - 'sudo lxc-create -n $LXC_NAME -t ubuntu -- -r $LXC_RELEASE --mirror http://mirrors.us.kernel.org/ubuntu --packages python || true' + - 'sudo lxc-start -n $LXC_NAME && until (sudo lxc-info -n $LXC_NAME | grep -q ^IP:); do printf . && sleep 1; done && sleep 2' + - export LXC_IP="$(sudo lxc-info -Hin $LXC_NAME)" + - sudo /bin/bash -c "printf '\n$LXC_IP test.lxc\n' >> /etc/hosts" + - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' + - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ + - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys + - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt + +script: + - ansible-playbook deploy.yml --syntax-check + - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" --skip-tags apparmor From 35f322aa4a06e90542aa15677b1194e53ec56a90 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 23:29:30 +0300 Subject: [PATCH 263/769] Do your job, travis! --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0d05220f..b3cde5ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ --- - language: python python: "2.7" sudo: required From 97dc868d2c45b3ae9ebf5ca8326b39bb76645fbe Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 11 Jan 2017 23:35:29 +0300 Subject: [PATCH 264/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58ad7b64..41db6bcd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Algo VPN -[![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) +[![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco)) is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From 38914fb8271eb3cb8de57316ac2008ff2709415c Mon Sep 17 00:00:00 2001 From: Tonimir Kisasondi Date: Fri, 13 Jan 2017 03:14:05 +0100 Subject: [PATCH 265/769] Updated README.md (#214) Just added -y to apt-get so it doesn't prompt for prerequisites install. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41db6bcd..f1bdebcd 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` - Linux (deb-based): `sudo easy_install pip && sudo apt-get update && sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo easy_install pip && sudo apt-get update && sudo apt-get install build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) From d23c952a4e67d247799e2a56bf44ab71735dba83 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 14 Jan 2017 19:37:47 +0300 Subject: [PATCH 266/769] Add the algo ssh key to any server (prevent fails when a user wants to update-users on a server deployed by algo but not with the algo ssh key) --- deploy.yml | 2 +- playbooks/common.yml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/deploy.yml b/deploy.yml index 649481e3..7b7e9ef3 100644 --- a/deploy.yml +++ b/deploy.yml @@ -7,7 +7,7 @@ pre_tasks: - name: Local pre-tasks include: playbooks/local.yml - tags: [ 'cloud' ] + tags: [ 'always' ] roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } diff --git a/playbooks/common.yml b/playbooks/common.yml index 36a051c6..c195b13d 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -5,3 +5,10 @@ raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 tags: - update-alternatives + +- name: Ensure the algo ssh key exist on the server + authorized_key: + user: "{{ ansible_ssh_user }}" + state: present + key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + tags: [ 'always' ] From c84abee047d8c525b729cc67812b64668b52c590 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 14 Jan 2017 19:38:03 +0300 Subject: [PATCH 267/769] increase timeouts --- ansible.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible.cfg b/ansible.cfg index e7173fa8..8c63b5ea 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -3,7 +3,7 @@ inventory = inventory pipelining = True retry_files_enabled = False host_key_checking = False -timeout = 30 +timeout = 60 [paramiko_connection] record_host_keys = False From 3e852caf041b0b36b21d9cc7d49007ad0cbd1fb3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 14 Jan 2017 19:56:23 +0300 Subject: [PATCH 268/769] disable compression #146 --- roles/vpn/templates/client_ipsec.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 32a71f79..ffdbcc89 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -3,7 +3,7 @@ conn ikev2-{{ IP_subject_alt_name }} rekey=no dpdaction=clear keyexchange=ikev2 - compress=yes + compress=no dpddelay=35s {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} From ea4e82d66d22cdc4aa3eddd2d5b23a4928c280e3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 14 Jan 2017 20:07:52 +0300 Subject: [PATCH 269/769] move troubleshooting from the landing readme page --- README.md | 49 +---------------------------------------- docs/Troubleshooting.md | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 48 deletions(-) create mode 100644 docs/Troubleshooting.md diff --git a/README.md b/README.md index f1bdebcd..fdae5875 100644 --- a/README.md +++ b/README.md @@ -125,54 +125,7 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). -## Troubleshooting - -### Error: "You have not agreed to the Xcode license agreements" - -On macOS, did you try to install the dependencies with pip and encounter the following error? - -``` -Downloading cffi-1.9.1.tar.gz (407kB): 407kB downloaded - Running setup.py (path:/private/tmp/pip_build_root/cffi/setup.py) egg_info for package cffi - -You have not agreed to the Xcode license agreements, please run 'xcodebuild -license' (for user-level acceptance) or 'sudo xcodebuild -license' (for system-wide acceptance) from within a Terminal window to review and agree to the Xcode license agreements. - - No working compiler found, or bogus compiler options - passed to the compiler from Python's distutils module. - See the error messages above. - (If they are about -mno-fused-madd and you are on OS/X 10.8, - see http://stackoverflow.com/questions/22313407/ .) - ----------------------------------------- -Cleaning up... -Command python setup.py egg_info failed with error code 1 in /private/tmp/pip_build_root/cffi -Storing debug log for failure in /Users/algore/Library/Logs/pip.log -``` - -The Xcode compiler is installed but requires you to accept its license agreement prior to using it. Run `xcodebuild -license` to agree and then retry installing the dependencies. - -### Error: "fatal error: 'openssl/opensslv.h' file not found" - -On macOS, did you try to install pycrypto and encounter the following error? - -``` -build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found - -#include - - ^ - -1 error generated. - -error: command 'cc' failed with exit status 1 - ----------------------------------------- -Cleaning up... -Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/pip_build_root/cryptography/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-sREEE5-record/install-record.txt --single-version-externally-managed --compile failed with error code 1 in /private/tmp/pip_build_root/cryptography -Storing debug log for failure in /Users/algore/Library/Logs/pip.log -``` - -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. +## [Troubleshooting](docs/Troubleshooting.md) ### Little Snitch is broken when connected to the VPN diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 00000000..2d82ba11 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,46 @@ +### Error: "You have not agreed to the Xcode license agreements" + +On macOS, did you try to install the dependencies with pip and encounter the following error? + +``` +Downloading cffi-1.9.1.tar.gz (407kB): 407kB downloaded + Running setup.py (path:/private/tmp/pip_build_root/cffi/setup.py) egg_info for package cffi + +You have not agreed to the Xcode license agreements, please run 'xcodebuild -license' (for user-level acceptance) or 'sudo xcodebuild -license' (for system-wide acceptance) from within a Terminal window to review and agree to the Xcode license agreements. + + No working compiler found, or bogus compiler options + passed to the compiler from Python's distutils module. + See the error messages above. + (If they are about -mno-fused-madd and you are on OS/X 10.8, + see http://stackoverflow.com/questions/22313407/ .) + +---------------------------------------- +Cleaning up... +Command python setup.py egg_info failed with error code 1 in /private/tmp/pip_build_root/cffi +Storing debug log for failure in /Users/algore/Library/Logs/pip.log +``` + +The Xcode compiler is installed but requires you to accept its license agreement prior to using it. Run `xcodebuild -license` to agree and then retry installing the dependencies. + +### Error: "fatal error: 'openssl/opensslv.h' file not found" + +On macOS, did you try to install pycrypto and encounter the following error? + +``` +build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found + +#include + + ^ + +1 error generated. + +error: command 'cc' failed with exit status 1 + +---------------------------------------- +Cleaning up... +Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/pip_build_root/cryptography/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-sREEE5-record/install-record.txt --single-version-externally-managed --compile failed with error code 1 in /private/tmp/pip_build_root/cryptography +Storing debug log for failure in /Users/algore/Library/Logs/pip.log +``` + +You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. From 1681b98eb2109716c6b1bdfb7d1b1dd140c263bf Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 14 Jan 2017 20:27:18 +0300 Subject: [PATCH 270/769] update the troubleshooting page #146 --- docs/Troubleshooting.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 2d82ba11..33faf5ab 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -44,3 +44,7 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. + +### Various parts of the internet appear to be offline through the VPN + +The issue may related to the MTU size, try to use `ping` with the don't fragment bit and various packet size in order to determine the MTU size for your network and set up this properly on the physical adapter. From 2798f84d3fdbaf8289ebbe9ec384a266d8ad4b1d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 16 Jan 2017 00:17:47 +0300 Subject: [PATCH 271/769] ensure that apparmor is supported by the kernel #215 --- .travis.yml | 2 +- roles/common/tasks/main.yml | 9 +++++++++ roles/dns_adblocking/tasks/main.yml | 2 ++ roles/proxy/tasks/main.yml | 2 ++ roles/vpn/tasks/main.yml | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b3cde5ea..76d8bb2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,4 +45,4 @@ install: script: - ansible-playbook deploy.yml --syntax-check - - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" --skip-tags apparmor + - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 79c7cfeb..1262d3fc 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -94,3 +94,12 @@ sysctl: name=net.ipv6.conf.all.forwarding value=1 tags: - always + +- name: Check apparmor support + shell: apparmor_status + ignore_errors: yes + register: apparmor_status + +- set_fact: + apparmor_enabled: true + when: '"profiles are in enforce mode" in apparmor_status.stdout' diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index e3692bbf..bf589319 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -6,6 +6,7 @@ - name: Dnsmasq profile for apparmor configured template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 + when: apparmor_enabled is defined and apparmor_enabled == true notify: - restart dnsmasq @@ -14,6 +15,7 @@ - name: Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq + when: apparmor_enabled is defined and apparmor_enabled == true tags: ['apparmor'] - name: Ensure that the dnsmasq service directory exist diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml index 9117dfb7..0af30dfc 100644 --- a/roles/proxy/tasks/main.yml +++ b/roles/proxy/tasks/main.yml @@ -14,11 +14,13 @@ - name: Privoxy profile for apparmor configured template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 + when: apparmor_enabled is defined and apparmor_enabled == true notify: - restart privoxy - name: Enforce the privoxy AppArmor policy shell: aa-enforce usr.sbin.privoxy + when: apparmor_enabled is defined and apparmor_enabled == true tags: ['apparmor'] - name: Ensure that the privoxy service directory exist diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 0ec3a180..1770ac54 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -15,6 +15,7 @@ - name: Enforcing ipsec with apparmor shell: aa-enforce "{{ item }}" + when: apparmor_enabled is defined and apparmor_enabled == true with_items: - /usr/lib/ipsec/charon - /usr/lib/ipsec/lookip From 2027d23c5511ae8d9c991d9232146fd5f652aa4f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 22 Jan 2017 22:56:17 +0300 Subject: [PATCH 272/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdae5875..6689abca 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` - Linux (deb-based): `sudo easy_install pip && sudo apt-get update && sudo apt-get install build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) From 569df11088e9f2598400fb03076b825ab80ab73b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 22 Jan 2017 23:06:32 +0300 Subject: [PATCH 273/769] Prevent ansible and Jinja2 from updates #220 (#221) --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 706af7f2..dac22242 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ansible>=2.1 +ansible>=2.1,<2.2.1 dopy==0.3.5 boto>=2.5 boto3 @@ -6,3 +6,4 @@ azure==2.0.0rc5 apache-libcloud six pyopenssl +jinja2==2.8 From 8d21923b702329683562c9a2d547f9477535768f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 26 Jan 2017 20:01:06 +0300 Subject: [PATCH 274/769] Additional info in the congrats --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index c5cbe0fd..af31bb78 100644 --- a/config.cfg +++ b/config.cfg @@ -75,7 +75,7 @@ congrats: | "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" - "# The p12 password is {{ easyrsa_p12_export_password }} #" + "# The p12 and SSH keys password is {{ easyrsa_p12_export_password }} #" "# The CA key password is {{ easyrsa_CA_password }} #" "#----------------------------------------------------------------------#" From 257be0f395ae18f4703ce403497d9a3e4edcb171 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 1 Feb 2017 18:54:47 +0300 Subject: [PATCH 275/769] make the fail message more understandable. Fixes #217 --- roles/cloud-digitalocean/tasks/main.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 34614855..b60a913e 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -3,16 +3,20 @@ do_token: "{{ do_access_token }}" public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" -- name: "Delete the existing Algo SSH keys" - digital_ocean: - state: absent - command: ssh - api_token: "{{ do_access_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - until: ssh_keys.changed != 1 - retries: 10 - delay: 1 +- block: + - name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_access_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 + rescue: + - fail: + msg: "Please, ensure that your API token is not read-only." - name: "Upload the SSH key" digital_ocean: From 35faf4bca733e40a01f9accb570eb04fc3936f7b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 3 Feb 2017 22:24:02 +0300 Subject: [PATCH 276/769] Local openssl tasks (#169) * Draft works with ECDSA RSA support for Windows * update-users with local_openssl_tasks * move prompts to the algo script * additional directory for SSH keys * move easyrsa_p12_export_password to pre_tasks * update-users testing * Fix hardcoded vars * Delete the CA key * Hardcoded IP. Fixes #219 * Some fixes --- .travis.yml | 5 +- algo | 32 +++- deploy.yml | 15 ++ roles/ssh_tunneling/tasks/main.yml | 6 +- roles/vpn/tasks/main.yml | 233 +++++++++++++++++---------- roles/vpn/templates/easy-rsa.vars.j2 | 202 ----------------------- roles/vpn/templates/mobileconfig.j2 | 2 +- roles/vpn/templates/openssl.cnf.j2 | 143 ++++++++++++++++ tests/update-users.sh | 27 ++++ users.yml | 167 ++++++++++++------- 10 files changed, 476 insertions(+), 356 deletions(-) delete mode 100644 roles/vpn/templates/easy-rsa.vars.j2 create mode 100644 roles/vpn/templates/openssl.cnf.j2 create mode 100755 tests/update-users.sh diff --git a/.travis.yml b/.travis.yml index 76d8bb2c..904dbdbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,4 +45,7 @@ install: script: - ansible-playbook deploy.yml --syntax-check - - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" + - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" + +after_script: + - ./tests/update-users.sh \ No newline at end of file diff --git a/algo b/algo index 26fbbd22..d001a6e5 100755 --- a/algo +++ b/algo @@ -50,6 +50,12 @@ Do you want the VPN to support Windows 10 clients? (requires RSA certificates an Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi +read -p " +Do you want to store the CA key? (required for update-users script, but less secure) +[y/N]: " -r Store_CAKEY +Store_CAKEY=${Store_CAKEY:-N} +if [[ "$Store_CAKEY" =~ ^(n|N)$ ]]; then EXTRA_VARS+=" Store_CAKEY=N"; fi + } deploy () { @@ -332,7 +338,31 @@ Enter the number of your desired provider } user_management () { - ansible-playbook users.yml + + read -p " +Enter IP address of your server: (use localhost for local installation) +: " -r server_ip + + read -p " +What user should we use to login on the server? (ignore if you're deploying to localhost) +[root]: " -r server_user + server_user=${server_user:-root} + +read -p " +Do you want each user to have their own account for SSH tunneling? +[y/N]: " -r ssh_tunneling_enabled +ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} + + read -p " +Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) +: " -r IP_subject + + read -p " +Enter the password for the private CA key: +[pasted values will not be displayed] +: " -rs easyrsa_CA_password + +ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" } case "$1" in diff --git a/deploy.yml b/deploy.yml index 7b7e9ef3..94a6c3d1 100644 --- a/deploy.yml +++ b/deploy.yml @@ -63,3 +63,18 @@ - debug: msg="{{ additional_information.split('\n') }}" tags: cloud + + - name: Save the CA key password + local_action: > + shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password + become: no + tags: tests + + - name: Delete the CA key + local_action: + module: file + path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + state: absent + become: no + tags: always + when: Store_CAKEY is defined and Store_CAKEY == "N" diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index b279b021..ba0baf29 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -57,13 +57,13 @@ template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - name: Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" - name: Change mode for SSH private keys - local_action: file path=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem mode=0600 + local_action: file path=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem mode=0600 with_items: "{{ users }}" become: false - name: Fetch the known_hosts file - fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}_known_hosts flat=yes + fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_known_hosts flat=yes diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 1770ac54..16b0bf14 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -9,6 +9,12 @@ - set_fact: easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" easyrsa_CA_password: "{{ CA_password.stdout }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Change the algorithm to RSA + set_fact: + algo_params: "rsa:2048" + when: Win10_Enabled is defined and Win10_Enabled == "Y" - name: Install StrongSwan apt: name=strongswan state=latest update_cache=yes @@ -97,153 +103,206 @@ when: item in strongswan_enabled_plugins with_items: "{{ strongswan_plugins.stdout_lines }}" -- name: Fetch easy-rsa-ipsec from git - git: - repo: git://github.com/ValdikSS/easy-rsa-ipsec.git - version: ipsec-with-patches - dest: "{{ easyrsa_dir }}" - -- name: Setup the vars file from our template - template: src=easy-rsa.vars.j2 dest={{ easyrsa_dir }}/easyrsa3/vars - - name: Ensure the pki directory is not exist - file: dest={{ easyrsa_dir }}/easyrsa3/pki state=absent + local_action: + module: file + dest: configs/{{ IP_subject_alt_name }}/pki + state: absent + become: no when: easyrsa_reinit_existent == True -- name: Build the pki enviroments - shell: > - ./easyrsa init-pki && - touch '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' - args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/pki_initialized' +- name: Ensure the pki directories are exist + local_action: + module: file + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: directory + recurse: yes + become: no + with_items: + - ecparams + - certs + - crl + - newcerts + - private + - reqs + +- name: Ensure the files are exist + local_action: + module: file + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: touch + become: no + with_items: + - ".rnd" + - "private/.rnd" + - "index.txt" + - "index.txt.attr" + - "serial" + +- name: Generate the openssl server configs + local_action: + module: template + src: openssl.cnf.j2 + dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" + become: no - name: Build the CA pair - shell: > - ./easyrsa --batch build-ca -- -passout pass:"{{ easyrsa_CA_password }}" && - touch {{ easyrsa_dir }}/easyrsa3/pki/ca_initialized + local_action: > + shell openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && + openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch -passout pass:"{{ easyrsa_CA_password }}" && + touch {{ IP_subject_alt_name }}_ca_generated + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/ca_initialized' - notify: - - restart strongswan + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: "{{ IP_subject_alt_name }}_ca_generated" + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + +- name: Copy the CA certificate + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" + mode: 0600 + become: no + +- name: Generate the serial number + local_action: > + shell echo 01 > serial && + touch serial_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: serial_generated - name: Build the server pair - shell: > - ./easyrsa gen-req {{ IP_subject_alt_name }} batch nopass -- -subj "/CN={{ IP_subject_alt_name }}" && - ./easyrsa --subject-alt-name='DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}' sign-req server {{ IP_subject_alt_name }} -- -passin pass:"{{ easyrsa_CA_password }}" && - touch '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && + openssl ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && + touch certs/{{ IP_subject_alt_name }}_crt_generated + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/server_initialized' - notify: - - restart strongswan + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ IP_subject_alt_name }}_crt_generated + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" - name: Build the client's pair - shell: > - ./easyrsa gen-req {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && - ./easyrsa --subject-alt-name='DNS:{{ item }}' sign-req client {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && + openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + touch certs/{{ item }}_crt_generated + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ item }}_crt_generated + environment: + subjectAltName: "DNS:{{ item }}" with_items: "{{ users }}" - name: Build the client's p12 - shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:"{{ easyrsa_p12_export_password }}" + local_action: > + shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" with_items: "{{ users }}" +- name: Copy the p12 certificates + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" + mode: 0600 + become: no + with_items: + - "{{ users }}" + - name: Copy the CA cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/ca.crt' dest=/etc/ipsec.d/cacerts/ca.crt owner=strongswan group=root mode=0600 + copy: src='configs/{{ IP_subject_alt_name }}/pki/cacert.pem' dest=/etc/ipsec.d/cacerts/ca.crt owner=strongswan group=root mode=0600 notify: - restart strongswan - name: Copy the server cert to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/issued/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=strongswan group=root mode=0600 + copy: src='configs/{{ IP_subject_alt_name }}/pki/certs/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=strongswan group=root mode=0600 notify: - restart strongswan - name: Copy the server key to the strongswan directory - copy: remote_src=True src='{{ easyrsa_dir }}/easyrsa3/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=strongswan group=root mode=0600 + copy: src='configs/{{ IP_subject_alt_name }}/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=strongswan group=root mode=0600 notify: - restart strongswan - name: Register p12 PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 + local_action: > + shell cat private/{{ item }}.p12 | base64 register: PayloadContent + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" with_items: "{{ users }}" -- name: Register CA PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 - register: PayloadContentCA - - name: Set facts for mobileconfigs set_fact: proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - name: Build the mobileconfigs - template: src=mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 + local_action: + module: template + src: mobileconfig.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + mode: 0600 + become: no with_together: - "{{ users }}" - "{{ PayloadContent.results }}" no_log: True - name: Build the client ipsec config file - template: src=client_ipsec.conf.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.conf mode=0600 + local_action: + module: template + src: client_ipsec.conf.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + mode: 0600 + become: no with_items: - "{{ users }}" - name: Build the client ipsec secret file - template: src=client_ipsec.secrets.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.secrets mode=0600 + local_action: + module: template + src: client_ipsec.secrets.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + mode: 0600 + become: no with_items: - "{{ users }}" -- name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes - with_items: "{{ users }}" - -- name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes - with_items: "{{ users }}" - -- name: Fetch users certificates - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt dest=configs/{{ IP_subject_alt_name }}_{{ item }}.crt flat=yes - with_items: "{{ users }}" - -- name: Fetch users keys - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key dest=configs/{{ IP_subject_alt_name }}_{{ item }}.key flat=yes - with_items: "{{ users }}" - -- name: Fetch users ipsec configs - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.conf dest=configs/{{ IP_subject_alt_name }}_{{ item }}_ipsec.conf flat=yes - with_items: "{{ users }}" - -- name: Fetch users ipsec secrets - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/ipsec_{{ item }}.secrets dest=configs/{{ IP_subject_alt_name }}_{{ item }}_ipsec.secrets flat=yes - with_items: "{{ users }}" - - name: Build the windows client powershell script - template: src=client_windows.ps1.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/windows_{{ item }}.ps1 mode=0600 + local_action: + module: template + src: client_windows.ps1.j2 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + mode: 0600 + become: no when: Win10_Enabled is defined and Win10_Enabled == "Y" with_items: "{{ users }}" -- name: Fetch users windows scripts - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/windows_{{ item }}.ps1 dest=configs/{{ IP_subject_alt_name }}_{{ item }}_windows.ps1 flat=yes - when: Win10_Enabled is defined and Win10_Enabled == "Y" - with_items: "{{ users }}" - -- name: Restrict permissions +- name: Restrict permissions for the remote private directories file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root with_items: - /etc/ipsec.d/private -- name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes +- name: Restrict permissions for the local private directories + local_action: + module: file + path: "{{ item }}" + state: directory + mode: 0700 + become: no + with_items: + - configs/{{ IP_subject_alt_name }} - include: iptables.yml tags: iptables diff --git a/roles/vpn/templates/easy-rsa.vars.j2 b/roles/vpn/templates/easy-rsa.vars.j2 deleted file mode 100644 index 2805b3b6..00000000 --- a/roles/vpn/templates/easy-rsa.vars.j2 +++ /dev/null @@ -1,202 +0,0 @@ -# Easy-RSA 3 parameter settings - -# NOTE: If you installed Easy-RSA from your distro's package manager, don't edit -# this file in place -- instead, you should copy the entire easy-rsa directory -# to another location so future upgrades don't wipe out your changes. - -# HOW TO USE THIS FILE -# -# vars.example contains built-in examples to Easy-RSA settings. You MUST name -# this file 'vars' if you want it to be used as a configuration file. If you do -# not, it WILL NOT be automatically read when you call easyrsa commands. -# -# It is not necessary to use this config file unless you wish to change -# operational defaults. These defaults should be fine for many uses without the -# need to copy and edit the 'vars' file. -# -# All of the editable settings are shown commented and start with the command -# 'set_var' -- this means any set_var command that is uncommented has been -# modified by the user. If you're happy with a default, there is no need to -# define the value to its default. - -# NOTES FOR WINDOWS USERS -# -# Paths for Windows *MUST* use forward slashes, or optionally double-esscaped -# backslashes (single forward slashes are recommended.) This means your path to -# the openssl binary might look like this: -# "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" - -# A little housekeeping: DON'T EDIT THIS SECTION -# -# Easy-RSA 3.x doesn't source into the environment directly. -# Complain if a user tries to do this: -if [ -z "$EASYRSA_CALLER" ]; then - echo "You appear to be sourcing an Easy-RSA 'vars' file." >&2 - echo "This is no longer necessary and is disallowed. See the section called" >&2 - echo "'How to use this file' near the top comments for more details." >&2 - return 1 -fi - -# DO YOUR EDITS BELOW THIS POINT - -# This variable should point to the top level of the easy-rsa tree. By default, -# this is taken to be the directory you are currently in. - -set_var EASYRSA "{{ easyrsa_dir }}/easyrsa3/" - -# If your OpenSSL command is not in the system PATH, you will need to define the -# path to it here. Normally this means a full path to the executable, otherwise -# you could have left it undefined here and the shown default would be used. -# -# Windows users, remember to use paths with forward-slashes (or escaped -# back-slashes.) Windows users should declare the full path to the openssl -# binary here if it is not in their system PATH. - -#set_var EASYRSA_OPENSSL "openssl" -# -# This sample is in Windows syntax -- edit it for your path if not using PATH: -#set_var EASYRSA_OPENSSL "C:/Program Files/OpenSSL-Win32/bin/openssl.exe" - -# Edit this variable to point to your soon-to-be-created key directory. -# -# WARNING: init-pki will do a rm -rf on this directory so make sure you define -# it correctly! (Interactive mode will prompt before acting.) - -set_var EASYRSA_PKI "$EASYRSA/pki" - -# Define X509 DN mode. -# This is used to adjust what elements are included in the Subject field as the DN -# (this is the "Distinguished Name.") -# Note that in cn_only mode the Organizational fields further below aren't used. -# -# Choices are: -# cn_only - use just a CN value -# org - use the "traditional" Country/Province/City/Org/OU/email/CN format - -set_var EASYRSA_DN "cn_only" - -# Organizational fields (used with 'org' mode and ignored in 'cn_only' mode.) -# These are the default values for fields which will be placed in the -# certificate. Don't leave any of these fields blank, although interactively -# you may omit any specific field by typing the "." symbol (not valid for -# email.) - -#set_var EASYRSA_REQ_COUNTRY "US" -#set_var EASYRSA_REQ_PROVINCE "California" -#set_var EASYRSA_REQ_CITY "San Francisco" -#set_var EASYRSA_REQ_ORG "Copyleft Certificate Co" -#set_var EASYRSA_REQ_EMAIL "me@example.net" -#set_var EASYRSA_REQ_OU "My Organizational Unit" - -# Choose a size in bits for your keypairs. The recommended value is 2048. Using -# 2048-bit keys is considered more than sufficient for many years into the -# future. Larger keysizes will slow down TLS negotiation and make key/DH param -# generation take much longer. Values up to 4096 should be accepted by most -# software. Only used when the crypto alg is rsa (see below.) - -# set_var EASYRSA_KEY_SIZE 2048 - -# The default crypto mode is rsa; ec can enable elliptic curve support. -# Note that not all software supports ECC, so use care when enabling it. -# Choices for crypto alg are: (each in lower-case) -# * rsa -# * ec - -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} -set_var EASYRSA_ALGO rsa -{% else %} -set_var EASYRSA_ALGO ec -{% endif %} - -# Define the named curve, used in ec mode only: - -set_var EASYRSA_CURVE prime256v1 - -# In how many days should the root CA key expire? - -set_var EASYRSA_CA_EXPIRE {{ easyrsa_ca_expire }} - -# In how many days should certificates expire? - -set_var EASYRSA_CERT_EXPIRE {{ easyrsa_cert_expire }} - -# How many days until the next CRL publish date? Note that the CRL can still be -# parsed after this timeframe passes. It is only used for an expected next -# publication date. - -#set_var EASYRSA_CRL_DAYS 180 - -# Support deprecated "Netscape" extensions? (choices "yes" or "no".) The default -# is "no" to discourage use of deprecated extensions. If you require this -# feature to use with --ns-cert-type, set this to "yes" here. This support -# should be replaced with the more modern --remote-cert-tls feature. If you do -# not use --ns-cert-type in your configs, it is safe (and recommended) to leave -# this defined to "no". When set to "yes", server-signed certs get the -# nsCertType=server attribute, and also get any NS_COMMENT defined below in the -# nsComment field. - -#set_var EASYRSA_NS_SUPPORT "no" - -# When NS_SUPPORT is set to "yes", this field is added as the nsComment field. -# Set this blank to omit it. With NS_SUPPORT set to "no" this field is ignored. - -#set_var EASYRSA_NS_COMMENT "Easy-RSA Generated Certificate" - -# A temp file used to stage cert extensions during signing. The default should -# be fine for most users; however, some users might want an alternative under a -# RAM-based FS, such as /dev/shm or /tmp on some systems. - -#set_var EASYRSA_TEMP_FILE "$EASYRSA_PKI/extensions.temp" - -# !! -# NOTE: ADVANCED OPTIONS BELOW THIS POINT -# PLAY WITH THEM AT YOUR OWN RISK -# !! - -# Broken shell command aliases: If you have a largely broken shell that is -# missing any of these POSIX-required commands used by Easy-RSA, you will need -# to define an alias to the proper path for the command. The symptom will be -# some form of a 'command not found' error from your shell. This means your -# shell is BROKEN, but you can hack around it here if you really need. These -# shown values are not defaults: it is up to you to know what you're doing if -# you touch these. -# -#alias awk="/alt/bin/awk" -#alias cat="/alt/bin/cat" - -# X509 extensions directory: -# If you want to customize the X509 extensions used, set the directory to look -# for extensions here. Each cert type you sign must have a matching filename, -# and an optional file named 'COMMON' is included first when present. Note that -# when undefined here, default behaviour is to look in $EASYRSA_PKI first, then -# fallback to $EASYRSA for the 'x509-types' dir. You may override this -# detection with an explicit dir here. -# -#set_var EASYRSA_EXT_DIR "$EASYRSA/x509-types" - -# OpenSSL config file: -# If you need to use a specific openssl config file, you can reference it here. -# Normally this file is auto-detected from a file named openssl-1.0.cnf from the -# EASYRSA_PKI or EASYRSA dir (in that order.) NOTE that this file is Easy-RSA -# specific and you cannot just use a standard config file, so this is an -# advanced feature. - -set_var EASYRSA_SSL_CONF "$EASYRSA/openssl-1.0.cnf" - -# Default CN: -# This is best left alone. Interactively you will set this manually, and BATCH -# callers are expected to set this themselves. - -set_var EASYRSA_REQ_CN "{{ IP_subject_alt_name }}" - -# Cryptographic digest to use. -# Do not change this default unless you understand the security implications. -# Valid choices include: md5, sha1, sha256, sha224, sha384, sha512 - -#set_var EASYRSA_DIGEST "sha256" - -# Batch mode. Leave this disabled unless you intend to call Easy-RSA explicitly -# in batch mode without any user input, confirmation on dangerous operations, -# or most output. Setting this to any non-blank string enables batch mode. - -set_var EASYRSA_BATCH "{{ IP_subject_alt_name }}" diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index d2873260..9ee20c4f 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -172,7 +172,7 @@ ca.crt PayloadContent - {{ PayloadContentCA.stdout }} + {{ PayloadContentCA }} PayloadDescription Adds a CA root certificate diff --git a/roles/vpn/templates/openssl.cnf.j2 b/roles/vpn/templates/openssl.cnf.j2 new file mode 100644 index 00000000..415557f8 --- /dev/null +++ b/roles/vpn/templates/openssl.cnf.j2 @@ -0,0 +1,143 @@ +# For use with Easy-RSA 3.0 and OpenSSL 1.0.* + +RANDFILE = .rnd + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = . # Where everything is kept +certs = $dir # Where the issued certs are kept +crl_dir = $dir # Where the issued crl are kept +database = $dir/index.txt # database index file. +new_certs_dir = $dir/certs # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem # The private key +RANDFILE = $dir/private/.rand # private random number file + +x509_extensions = basic_exts # The extentions to add to the cert + +# This allows a V2 CRL. Ancient browsers don't like it, but anything Easy-RSA +# is designed for will. In return, we get the Issuer attached to CRLs. +crl_extensions = crl_ext + +default_days = 3650 # how long to certify for +default_crl_days= 3650 # how long before next CRL +default_md = sha256 # use public key default MD +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_anything + +# For the 'anything' policy, which defines allowed DN fields +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +name = optional +emailAddress = optional + +#################################################################### +# Easy-RSA request handling +# We key off $DN_MODE to determine how to format the DN +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +default_md = sha256 +distinguished_name = cn_only +x509_extensions = easyrsa_ca # The extentions to add to the self signed cert + +# A placeholder to handle the $EXTRA_EXTS feature: +#%EXTRA_EXTS% # Do NOT remove or change this line as $EXTRA_EXTS support requires it + +#################################################################### +# Easy-RSA DN (Subject) handling + +# Easy-RSA DN for cn_only support: +[ cn_only ] +commonName = Common Name (eg: your user, host, or server name) +commonName_max = 64 +commonName_default = {{ IP_subject_alt_name }} + +# Easy-RSA DN for org support: +[ org ] +countryName = Country Name (2 letter code) +countryName_default = US +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = California + +localityName = Locality Name (eg, city) +localityName_default = San Francisco + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Copyleft Certificate Co + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = My Organizational Unit + +commonName = Common Name (eg: your user, host, or server name) +commonName_max = 64 +commonName_default = {{ IP_subject_alt_name }} + +emailAddress = Email Address +emailAddress_default = me@example.net +emailAddress_max = 64 + +#################################################################### +# Easy-RSA cert extension handling + +# This section is effectively unused as the main script sets extensions +# dynamically. This core section is left to support the odd usecase where +# a user calls openssl directly. +[ basic_exts ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always + +extendedKeyUsage = serverAuth,1.3.6.1.5.5.7.3.17 +keyUsage = digitalSignature, keyEncipherment +subjectAltName = ${ENV::subjectAltName} + +# The Easy-RSA CA extensions +[ easyrsa_ca ] + +# PKIX recommendations: + +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer:always + +# This could be marked critical, but it's nice to support reading by any +# broken clients who attempt to do so. +basicConstraints = CA:true + +# Limit key usage to CA tasks. If you really want to use the generated pair as +# a self-signed cert, comment this out. +keyUsage = cRLSign, keyCertSign + +# nsCertType omitted by default. Let's try to let the deprecated stuff die. +# nsCertType = sslCA + +# CRL extensions. +[ crl_ext ] + +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always,issuer:always + + + diff --git a/tests/update-users.sh b/tests/update-users.sh new file mode 100755 index 00000000..b0cbb192 --- /dev/null +++ b/tests/update-users.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -e + +CAPW=`cat /tmp/ca_password` + +sed -i 's/- jack$/- jack_test/' config.cfg + +ansible-playbook users.yml -e "server_ip=$LXC_IP server_user=root ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" + +cd configs/$LXC_IP/pki/ + +if openssl crl -inform pem -noout -text -in crl/jack.crt | grep CRL + then + echo "The CRL check passed" + else + echo "The CRL check failed" + exit 1 +fi + +if openssl x509 -inform pem -noout -text -in certs/jack_test.crt | grep CN=jack_test + then + echo "The new user exist" + else + echo "The new user does not exist" + exit 1 +fi diff --git a/users.yml b/users.yml index b6f71307..c9837a26 100644 --- a/users.yml +++ b/users.yml @@ -4,30 +4,6 @@ gather_facts: False vars_files: - config.cfg - vars_prompt: - - - name: "server_ip" - prompt: "Enter IP address of your server: (use localhost for local installation)\n" - default: localhost - private: no - - - name: "server_user" - prompt: "What user should we use to login on the server? (ignore if you're deploying to localhost):\n" - default: "root" - private: no - - - name: "ssh_tunneling_enabled" - prompt: "Do you want each user to have their own account for SSH tunneling? (y/n):\n" - default: "n" - private: no - - - name: "IP_subject" - prompt: "Enter public IP address of your server: (IMPORTANT! This IP is used to verify the certificate)\n" - private: no - - - name: "easyrsa_CA_password" - prompt: "Enter the password for the private CA key:\n" - private: yes tasks: - name: Add the server to the vpn-host group @@ -39,6 +15,7 @@ ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" easyrsa_CA_password: "{{ easyrsa_CA_password }}" IP_subject: "{{ IP_subject }}" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - name: Wait until SSH becomes ready... local_action: @@ -53,7 +30,7 @@ - name: User management hosts: vpn-host - gather_facts: false + gather_facts: true become: true vars_files: - config.cfg @@ -61,6 +38,7 @@ pre_tasks: - set_fact: IP_subject_alt_name: "{{ IP_subject }}" + easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" roles: - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } @@ -70,73 +48,136 @@ - name: Gather Facts setup: - - set_fact: - easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + - name: Cheking the signature algorithm + local_action: > + shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 + become: no + register: sig_algo + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + + - name: Change the algorithm to RSA + set_fact: + algo_params: "rsa:2048" + when: '"ecdsa" not in sig_algo.stdout' - name: Build the client's pair - shell: > - ./easyrsa gen-req {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && - ./easyrsa --subject-alt-name='DNS:{{ item }}' sign-req client {{ item }} nopass -- -passin pass:"{{ easyrsa_CA_password }}" && - touch '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && + openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + touch certs/{{ item }}_crt_generated + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' - creates: '{{ easyrsa_dir }}/easyrsa3/pki/{{ item }}_initialized' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ item }}_crt_generated + environment: + subjectAltName: "DNS:{{ item }}" with_items: "{{ users }}" - name: Build the client's p12 - shell: > - openssl pkcs12 -in {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt -inkey {{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.key -export -name {{ item }} -out /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 -certfile {{ easyrsa_dir }}/easyrsa3//pki/ca.crt -passout pass:{{ easyrsa_p12_export_password }} + local_action: > + shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" with_items: "{{ users }}" + - name: Copy the p12 certificates + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" + mode: 0600 + become: no + with_items: + - "{{ users }}" + - name: Get active users - shell: > - grep ^V pki/index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + local_action: > + shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" register: valid_certs - name: Revoke non-existing users - shell: > - openssl ec -in pki/private/ca.key -out pki/private/ca.key -passin pass:"{{ easyrsa_CA_password }}" -passout pass:"" && - ipsec pki --signcrl --cacert {{ easyrsa_dir }}/easyrsa3//pki/ca.crt --cakey {{ easyrsa_dir }}/easyrsa3/pki/private/ca.key --reason superseded --cert {{ easyrsa_dir }}/easyrsa3//pki/issued/{{ item }}.crt > /etc/ipsec.d/crls/{{ item }}.der && - ./easyrsa revoke {{ item }} && - openssl ec -aes256 -in pki/private/ca.key -out pki/private/ca.key -passin pass:"" -passout pass:"{{ easyrsa_CA_password }}" && - ipsec rereadcrls + local_action: > + shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && + openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt + touch crl/{{ item }}_revoked + become: no args: - chdir: '{{ easyrsa_dir }}/easyrsa3/' + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: crl/{{ item }}_revoked + environment: + subjectAltName: "DNS:{{ item }}" when: item not in users with_items: "{{ valid_certs.stdout_lines }}" + - name: Copy the revoked certificates to the vpn server + copy: + src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt + dest: /etc/ipsec.d/crls/{{ item }}.crt + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" + notify: + - rereadcrls + - name: Register p12 PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 + local_action: > + shell cat private/{{ item }}.p12 | base64 register: PayloadContent + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" with_items: "{{ users }}" - - name: Register CA PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 - register: PayloadContentCA + - name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - name: Build the mobileconfigs - template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}.mobileconfig mode=0600 + local_action: + module: template + src: roles/vpn/templates/mobileconfig.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + mode: 0600 + become: no with_together: - "{{ users }}" - "{{ PayloadContent.results }}" no_log: True - - name: Fetch users P12 - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 dest=configs/{{ IP_subject_alt_name }}_{{ item }}.p12 flat=yes - with_items: "{{ users }}" + - name: Build the client ipsec config file + local_action: + module: template + src: roles/vpn/templates/client_ipsec.conf.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + mode: 0600 + become: no + with_items: + - "{{ users }}" - - name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}.mobileconfig flat=yes - with_items: "{{ users }}" + - name: Build the client ipsec secret file + local_action: + module: template + src: roles/vpn/templates/client_ipsec.secrets.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + mode: 0600 + become: no + with_items: + - "{{ users }}" - - name: Fetch server CA certificate - fetch: src=/{{ easyrsa_dir }}/easyrsa3/pki/ca.crt dest=configs/{{ IP_subject_alt_name }}_ca.crt flat=yes + - name: Build the windows client powershell script + local_action: + module: template + src: roles/vpn/templates/client_windows.ps1.j2 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + mode: 0600 + become: no + when: Win10_Enabled is defined and Win10_Enabled == "Y" + with_items: "{{ users }}" # SSH @@ -163,3 +204,7 @@ post_tasks: - debug: msg="{{ congrats.split('\n') }}" tags: always + + handlers: + - name: rereadcrls + shell: ipsec rereadcrls From 26168f10a00712be595a1759c97c422657c9756f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 16:35:23 -0500 Subject: [PATCH 277/769] Closes #82, again --- logo.png | Bin 58315 -> 10263 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/logo.png b/logo.png index c33787bd174e93bef28dee0834afcc13ad3ec2aa..e72f5b13af2d805ff5737b8b8dc8c0484dbfc869 100644 GIT binary patch literal 10263 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 literal 58315 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�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 From 2559c264c6e0455f02d47204a97f32ee73b4fd63 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 16:46:58 -0500 Subject: [PATCH 278/769] Slightly better docs --- CONTRIBUTING.md | 5 +++-- README.md | 6 ++---- docs/Troubleshooting.md | 8 ++++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b5b936c..314a5d9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,14 @@ ### Troubleshooting -* Check that you installed all the dependencies with pip and have Ansible 2.1+ +* Check that all necessary dependencies are installed, including the correct version of Ansible * If installing to a local server, use a fresh install of Ubuntu 16.04 +* Check that your issue is not already described in the [troubleshooting](docs/TROUBLESHOOTING.md) docs ### Filing New Issues * Please review the [FAQ](https://github.com/trailofbits/algo#faq) * Please include the full output from your terminal window if appropriate -* We only support macOS 10.11 and newer +* We only support macOS 10.11+, Windows 8+, and Ubuntu 16.04+ ### Pull Requests diff --git a/README.md b/README.md index 6689abca..54335c0f 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,6 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). -## [Troubleshooting](docs/Troubleshooting.md) +## [Troubleshooting](docs/TROUBLESHOOTING.md) -### Little Snitch is broken when connected to the VPN - -Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). +If you have issues deploying and using the Algo VPN server, check the [troubleshooting](docs/TROUBLESHOOTING.md) documentation for solutions to common problems. \ No newline at end of file diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 33faf5ab..3e4adf9a 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -45,6 +45,10 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. -### Various parts of the internet appear to be offline through the VPN +### Little Snitch is broken when connected to the VPN -The issue may related to the MTU size, try to use `ping` with the don't fragment bit and various packet size in order to determine the MTU size for your network and set up this properly on the physical adapter. +Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). + +### Various websites appear to be offline through the VPN + +This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend filing a ticket for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. \ No newline at end of file From e95ee10c3cf3260c0244468dfac7133b4325cea4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:01:31 -0500 Subject: [PATCH 279/769] slightly better docs --- CONTRIBUTING.md | 18 +++++++----------- README.md | 6 +++++- docs/Troubleshooting.md | 15 +++++++++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 314a5d9e..d8052f3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,16 +1,12 @@ -### Troubleshooting - -* Check that all necessary dependencies are installed, including the correct version of Ansible -* If installing to a local server, use a fresh install of Ubuntu 16.04 -* Check that your issue is not already described in the [troubleshooting](docs/TROUBLESHOOTING.md) docs - ### Filing New Issues -* Please review the [FAQ](https://github.com/trailofbits/algo#faq) -* Please include the full output from your terminal window if appropriate -* We only support macOS 10.11+, Windows 8+, and Ubuntu 16.04+ +* Check that your issue is not already described in the [FAQ](https://github.com/trailofbits/algo#faq) or [troubleshooting](docs/TROUBLESHOOTING.md) docs +* Check that all necessary dependencies are installed, including the correct version of Ansible +* Algo VPN client supported is limited to only modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. +* If your issue has not been resolved by the above, file an issue and fill out the requested information from the Issue Template ### Pull Requests -* Run [ansible-lint](https://github.com/willthames/ansible-lint) on any new ansible scripts -* Run [shellcheck](https://github.com/koalaman/shellcheck) on any new shell scripts +* Run [ansible-lint](https://github.com/willthames/ansible-lint) or [shellcheck](https://github.com/koalaman/shellcheck) on any new scripts + +Thanks! \ No newline at end of file diff --git a/README.md b/README.md index 54335c0f..14d3ba67 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) -Algo VPN (short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco)) is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. +Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. ## Features @@ -125,6 +125,10 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). +### Where did the name "Algo" come from? + +Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). + ## [Troubleshooting](docs/TROUBLESHOOTING.md) If you have issues deploying and using the Algo VPN server, check the [troubleshooting](docs/TROUBLESHOOTING.md) documentation for solutions to common problems. \ No newline at end of file diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index 3e4adf9a..e25add48 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -1,4 +1,11 @@ -### Error: "You have not agreed to the Xcode license agreements" +## Table of Contents + +1. [Error: "You have not agreed to the Xcode license agreements"](#error-you-have-not-agreed-to-the-xcode-license-agreements) +2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#error-fatal-error-opensslopensslvh-file-not-found) +3. [Little Snitch is broken when connected to the VPN](#little-snitch-is-broken-when-connected-to-the-vpn) +4. [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) + +### 1. Error: "You have not agreed to the Xcode license agreements" On macOS, did you try to install the dependencies with pip and encounter the following error? @@ -22,7 +29,7 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log The Xcode compiler is installed but requires you to accept its license agreement prior to using it. Run `xcodebuild -license` to agree and then retry installing the dependencies. -### Error: "fatal error: 'openssl/opensslv.h' file not found" +### 2. Error: "fatal error: 'openssl/opensslv.h' file not found" On macOS, did you try to install pycrypto and encounter the following error? @@ -45,10 +52,10 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. -### Little Snitch is broken when connected to the VPN +### 3. Little Snitch is broken when connected to the VPN Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). -### Various websites appear to be offline through the VPN +### 4. Various websites appear to be offline through the VPN This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend filing a ticket for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. \ No newline at end of file From 2f9417e659dee2ae675dbe3ab58f0c5961d7ffd0 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:02:18 -0500 Subject: [PATCH 280/769] Update Troubleshooting.md --- docs/Troubleshooting.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md index e25add48..86348cd7 100644 --- a/docs/Troubleshooting.md +++ b/docs/Troubleshooting.md @@ -1,9 +1,9 @@ ## Table of Contents -1. [Error: "You have not agreed to the Xcode license agreements"](#error-you-have-not-agreed-to-the-xcode-license-agreements) -2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#error-fatal-error-opensslopensslvh-file-not-found) -3. [Little Snitch is broken when connected to the VPN](#little-snitch-is-broken-when-connected-to-the-vpn) -4. [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) +1. [Error: "You have not agreed to the Xcode license agreements"](#1-error-you-have-not-agreed-to-the-xcode-license-agreements) +2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) +3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) +4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -58,4 +58,4 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and ### 4. Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend filing a ticket for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. \ No newline at end of file +This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend filing a ticket for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. From d9b13cbd45a797a607364398a4cf0a38e8acd50b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:08:44 -0500 Subject: [PATCH 281/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8052f3a..317da9ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ ### Filing New Issues * Check that your issue is not already described in the [FAQ](https://github.com/trailofbits/algo#faq) or [troubleshooting](docs/TROUBLESHOOTING.md) docs -* Check that all necessary dependencies are installed, including the correct version of Ansible -* Algo VPN client supported is limited to only modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. -* If your issue has not been resolved by the above, file an issue and fill out the requested information from the Issue Template +* Did you remember to install the dependencies for your operating system prior to installing Algo? +* Client supported is limited to only modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. +* If you need to file an issue, fill out any relevant fields in the Issue Template ### Pull Requests * Run [ansible-lint](https://github.com/willthames/ansible-lint) or [shellcheck](https://github.com/koalaman/shellcheck) on any new scripts -Thanks! \ No newline at end of file +Thanks! From a94c4275961e0305bd72c34fc4e7c2462ae915bd Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:27:13 -0500 Subject: [PATCH 282/769] Move FAQ to its own doc. --- CONTRIBUTING.md | 2 +- README.md | 39 +++++++------------ docs/FAQ.md | 37 ++++++++++++++++++ ...{Troubleshooting.md => TROUBLESHOOTING.md} | 0 4 files changed, 52 insertions(+), 26 deletions(-) create mode 100644 docs/FAQ.md rename docs/{Troubleshooting.md => TROUBLESHOOTING.md} (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 317da9ca..5b37f56f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ ### Filing New Issues -* Check that your issue is not already described in the [FAQ](https://github.com/trailofbits/algo#faq) or [troubleshooting](docs/TROUBLESHOOTING.md) docs +* Check that your issue is not already described in the [FAQ](docs/FAQ.md) or [troubleshooting](docs/TROUBLESHOOTING.md) docs * Did you remember to install the dependencies for your operating system prior to installing Algo? * Client supported is limited to only modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. * If you need to file an issue, fill out any relevant fields in the Issue Template diff --git a/README.md b/README.md index 14d3ba67..157a6ce6 100644 --- a/README.md +++ b/README.md @@ -99,36 +99,25 @@ Algo's own scripts can easily add and remove users from the VPN server. The Algo VPN server now contains only the users listed in the `config.cfg` file. -## FAQ +## Documentation -### Has Algo been audited? +* The [FAQ](docs/FAQ.md) includes answers to common questions. +* The [Troubleshooting](docs/TROUBLESHOOTING.md) doc includes answers to common technical issues. +* The [Roles](docs/ROLES.md) doc includes a description of optional Algo VPN server features. +* The [Advanced Usage](docs/ADVANCED.md) doc describes how to run Algo VPN directly from Ansible. -No. This project is under [active development](https://github.com/trailofbits/algo/projects/1). We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). +## Endorsements -### Why aren't you using Tor? +> I've been ranting about the sorry state of VPN svcs for so long, probably about +> time to give a proper talk on the subject. TL;DR: use Algo. -The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). +-- [Kenn White](https://twitter.com/kennwhite/status/814166603587788800) -### Why aren't you using Racoon, LibreSwan, or OpenSwan? +> Before picking a VPN provider/app, make sure you do some research +> https://research.csiro.au/ng/wp-content/uploads/sites/106/2016/08/paper-1.pdf ... – or consider Algo -Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for StrongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. StrongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. +-- [The Register](https://twitter.com/TheRegister/status/825076303657177088) -### Why aren't you using a memory-safe or verified IKE daemon? +> Algo is really easy and secure. -I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to StrongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. - -### Why aren't you using OpenVPN? - -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). - -### Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? - -Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). - -### Where did the name "Algo" come from? - -Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). - -## [Troubleshooting](docs/TROUBLESHOOTING.md) - -If you have issues deploying and using the Algo VPN server, check the [troubleshooting](docs/TROUBLESHOOTING.md) documentation for solutions to common problems. \ No newline at end of file +-- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) \ No newline at end of file diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 00000000..26a9c4f8 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,37 @@ +## FAQ + +1. Has Algo been audited? +2. Why aren't you using Tor? +3. Why aren't you using Racoon, LibreSwan, or OpenSwan? +4. Why aren't you using a memory-safe or verified IKE daemon? +5. Why aren't you using OpenVPN? +6. Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? +7. Where did the name "Algo" come from? + +### 1. Has Algo been audited? + +No. This project is under [active development](https://github.com/trailofbits/algo/projects/1). We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). + +### 2. Why aren't you using Tor? + +The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). + +### 3. Why aren't you using Racoon, LibreSwan, or OpenSwan? + +Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for StrongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. StrongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. + +### 4. Why aren't you using a memory-safe or verified IKE daemon? + +I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to StrongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. + +### 5. Why aren't you using OpenVPN? + +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). + +### 6. Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? + +Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). + +### 7. Where did the name "Algo" come from? + +Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). \ No newline at end of file diff --git a/docs/Troubleshooting.md b/docs/TROUBLESHOOTING.md similarity index 100% rename from docs/Troubleshooting.md rename to docs/TROUBLESHOOTING.md From 013a3ca3214adc72ebe75f43c0e089f4a600bd93 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:29:17 -0500 Subject: [PATCH 283/769] TOC --- docs/FAQ.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 26a9c4f8..10fb6f35 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,12 +1,12 @@ ## FAQ -1. Has Algo been audited? -2. Why aren't you using Tor? -3. Why aren't you using Racoon, LibreSwan, or OpenSwan? -4. Why aren't you using a memory-safe or verified IKE daemon? -5. Why aren't you using OpenVPN? -6. Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? -7. Where did the name "Algo" come from? +1. [Has Algo been audited?](#1-has-algo-been-audited) +2. [Why aren't you using Tor?](#2-why-arent-you-using-tor) +3. [Why aren't you using Racoon, LibreSwan, or OpenSwan?](#3-why-arent-you-using-racoon-libreswan-or-openswan) +4. [Why aren't you using a memory-safe or verified IKE daemon?](#4-why-arent-you-using-a-memory-safe-or-verified-ike-daemon) +5. [Why aren't you using OpenVPN?](#5-why-arent-you-using-openvpn) +6. [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#6-why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) +7. [Where did the name "Algo" come from?](#7-where-did-the-name-algo-come-from) ### 1. Has Algo been audited? @@ -34,4 +34,4 @@ Alpine Linux is not supported out-of-the-box by any major cloud provider. We are ### 7. Where did the name "Algo" come from? -Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). \ No newline at end of file +Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). From f0d10b200a78f92186858500d3951a8b56d62cb4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:30:15 -0500 Subject: [PATCH 284/769] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 157a6ce6..f988b22a 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Documentation -* The [FAQ](docs/FAQ.md) includes answers to common questions. -* The [Troubleshooting](docs/TROUBLESHOOTING.md) doc includes answers to common technical issues. -* The [Roles](docs/ROLES.md) doc includes a description of optional Algo VPN server features. -* The [Advanced Usage](docs/ADVANCED.md) doc describes how to run Algo VPN directly from Ansible. +* [Advanced Usage](docs/ADVANCED.md) describes how to run Algo VPN directly from Ansible. +* [FAQ](docs/FAQ.md) includes answers to common questions. +* [Roles](docs/ROLES.md) includes a description of optional Algo VPN server features. +* [Troubleshooting](docs/TROUBLESHOOTING.md) includes answers to common technical issues. ## Endorsements @@ -120,4 +120,4 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. > Algo is really easy and secure. --- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) \ No newline at end of file +-- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) From 4567d280f7b8ee30942ca8a6d221da3dba994fcc Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 7 Feb 2017 17:31:13 -0500 Subject: [PATCH 285/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f988b22a..3c8222ff 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Documentation -* [Advanced Usage](docs/ADVANCED.md) describes how to run Algo VPN directly from Ansible. +* [Advanced Usage](docs/ADVANCED.md) describes how to deploy an Algo VPN server directly from Ansible. * [FAQ](docs/FAQ.md) includes answers to common questions. * [Roles](docs/ROLES.md) includes a description of optional Algo VPN server features. * [Troubleshooting](docs/TROUBLESHOOTING.md) includes answers to common technical issues. From d334e42048cdccd1e9226e22059d69c7a44f8447 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 12 Feb 2017 13:07:11 -0500 Subject: [PATCH 286/769] explicit pointer to avenues for support --- README.md | 2 +- docs/TROUBLESHOOTING.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c8222ff..6e24f61d 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Algo's own scripts can easily add and remove users from the VPN server. The Algo VPN server now contains only the users listed in the `config.cfg` file. -## Documentation +## Additional Documentation * [Advanced Usage](docs/ADVANCED.md) describes how to deploy an Algo VPN server directly from Ansible. * [FAQ](docs/FAQ.md) includes answers to common questions. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 86348cd7..d7e633ef 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -4,6 +4,7 @@ 2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) 3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) +5. [I have a problem not covered here](#5-i-have-a-problem-not-covered here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -58,4 +59,9 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and ### 4. Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend filing a ticket for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. + +### 5. I have a problem not covered here + +If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. + From 0422fe4c9e82c13ec84145701cf5637275e6dff8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 12 Feb 2017 13:13:24 -0500 Subject: [PATCH 287/769] typo --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index d7e633ef..3c627c22 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -4,7 +4,7 @@ 2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) 3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) -5. [I have a problem not covered here](#5-i-have-a-problem-not-covered here) +5. [I have a problem not covered here](#5-i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From 05ab1f5feb15f4b2145b71b6f0cfd035282c9fe9 Mon Sep 17 00:00:00 2001 From: akirilov Date: Sun, 12 Feb 2017 11:45:36 -0800 Subject: [PATCH 288/769] Modified certificate generation to address issues #234 and #228 (#235) * Modified certificate generation to address issues #234 and #228 I have made the following modifications to comply with the IKEv2 client certificate requirements: - Changed client certificate CN to {{ IP_subject_alt_name }}_{{ item }} from {{ item }} - Changed client certificate SAN to {{IP_subject_alt_name }} from {{ item }} - Added clientAuth to client certificate EKU I have made the following changes to address a mismatch in the windows deployment script and file names: - Changed the client certificate (.p12) filename in config/{{ IP_subject_alt_name }} to {{ IP_subject_alt_name}}_{{ item }}.p12 from {{ item }}.p12 to match the ps1 script Testing: I have tested the changes on Windows 10 client, Ubuntu 16.04.1 server (DigitalOcean) - the config described in Issue #234 I apologize for not being able to test on other configurations. I hope that someone else can verify my changes * fixed iOS issues * fixed accidentall user change * simplified changes * Final iteration. I think that's all I can do to minimize the changes --- roles/vpn/templates/client_windows.ps1.j2 | 2 +- roles/vpn/templates/openssl.cnf.j2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 9b6d1970..dfa1ebd7 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,3 @@ -certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ IP_subject_alt_name }}_{{ item }}.p12 +certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 Add-VpnConnection -name "Algo" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none diff --git a/roles/vpn/templates/openssl.cnf.j2 b/roles/vpn/templates/openssl.cnf.j2 index 415557f8..9ec12b2d 100644 --- a/roles/vpn/templates/openssl.cnf.j2 +++ b/roles/vpn/templates/openssl.cnf.j2 @@ -108,7 +108,7 @@ basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always -extendedKeyUsage = serverAuth,1.3.6.1.5.5.7.3.17 +extendedKeyUsage = serverAuth,clientAuth,1.3.6.1.5.5.7.3.17 keyUsage = digitalSignature, keyEncipherment subjectAltName = ${ENV::subjectAltName} From 20ebd7a5956d4c94c50beb392568c76613e35cb1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 12 Feb 2017 23:01:29 +0300 Subject: [PATCH 289/769] rename connection --- roles/vpn/templates/client_windows.ps1.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index dfa1ebd7..aa5b708b 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,3 @@ certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 -Add-VpnConnection -name "Algo" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none +Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none From dd3b9b9a1845633aa4befd6fc0203774919c66b4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 13 Feb 2017 09:57:45 +0100 Subject: [PATCH 290/769] twitter badge --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e24f61d..0827c89b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Algo VPN -[![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) +[![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) +[![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) +[![Twitter Follow](https://img.shields.io/twitter/follow/AlgoVPN.svg?style=social&label=Follow)](https://twitter.com/AlgoVPN) Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From 90d56aaea35c462d04a4ffee86aea25f0306e987 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 13 Feb 2017 10:10:50 +0100 Subject: [PATCH 291/769] remove twitter button :-( --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0827c89b..c8edb522 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) -[![Twitter Follow](https://img.shields.io/twitter/follow/AlgoVPN.svg?style=social&label=Follow)](https://twitter.com/AlgoVPN) Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From 79116f898a43af7a6be35d40ccdd82b1f79218a5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 14 Feb 2017 17:39:58 +0100 Subject: [PATCH 292/769] Update README.md --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8edb522..51ee7cfa 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,21 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 5. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). -That's it! You now have an Algo VPN server on the internet. +That's it! You now have an Algo VPN server on the internet. You will get a message like the one below when the server deployment process completes and your server is ready. Take note of the p12 password in case you need it later. -Note: for local or scripted deployment instructions see the [Advanced Usage](/docs/ADVANCED.md) documentation. +``` + "\"#----------------------------------------------------------------------#\"", + "\"# Congratulations! #\"", + "\"# Your Algo server is running. #\"", + "\"# Config files and certificates are in the ./configs/ directory. #\"", + "\"# Go to https://whoer.net/ after connecting #\"", + "\"# and ensure that all your traffic passes through the VPN. #\"", + "\"# Local DNS resolver and Proxy IP address: 172.16.0.1 #\"", + "\"# The p12 and SSH keys password is a3044565 #\"", + "\"#----------------------------------------------------------------------#\"", +``` + +Note: Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. ## Configure the VPN Clients From 8bbccc3cb90ff5d00feba1f1b32fd8e26a32fc3e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 14 Feb 2017 17:42:54 +0100 Subject: [PATCH 293/769] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51ee7cfa..46bc80f9 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 5. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). -That's it! You now have an Algo VPN server on the internet. You will get a message like the one below when the server deployment process completes and your server is ready. Take note of the p12 password in case you need it later. +That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. ``` "\"#----------------------------------------------------------------------#\"", @@ -51,7 +51,7 @@ That's it! You now have an Algo VPN server on the internet. You will get a messa "\"# Go to https://whoer.net/ after connecting #\"", "\"# and ensure that all your traffic passes through the VPN. #\"", "\"# Local DNS resolver and Proxy IP address: 172.16.0.1 #\"", - "\"# The p12 and SSH keys password is a3044565 #\"", + "\"# The p12 and SSH keys password is XXXXXXXX #\"", "\"#----------------------------------------------------------------------#\"", ``` From 6cc3598cc642ff84ff5c7ceebd698b8e77dfaf6e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 14 Feb 2017 20:25:56 +0300 Subject: [PATCH 294/769] rewrite congrats --- config.cfg | 31 ++++++++++++++----------------- deploy.yml | 14 ++++++++++---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/config.cfg b/config.cfg index af31bb78..9ac69db6 100644 --- a/config.cfg +++ b/config.cfg @@ -67,23 +67,20 @@ CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" BetweenClients_DROP: Y -congrats: | - "#----------------------------------------------------------------------#" - "# Congratulations! #" - "# Your Algo server is running. #" - "# Config files and certificates are in the ./configs/ directory. #" - "# Go to https://whoer.net/ after connecting #" - "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" - "# The p12 and SSH keys password is {{ easyrsa_p12_export_password }} #" - "# The CA key password is {{ easyrsa_CA_password }} #" - "#----------------------------------------------------------------------#" - -additional_information: | - "#----------------------------------------------------------------------#" - "# Shell access: ssh -i {{ ansible_ssh_private_key_file }} {{ ansible_ssh_user }}@{{ ansible_ssh_host }} #" - "#----------------------------------------------------------------------#" - +congrats: + common: | + "# Congratulations! #" + "# Your Algo server is running. #" + "# Config files and certificates are in the ./configs/ directory. #" + "# Go to https://whoer.net/ after connecting #" + "# and ensure that all your traffic passes through the VPN. #" + "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" + p12_pass: | + "# The p12 and SSH keys password is {{ easyrsa_p12_export_password }} #" + ca_key_pass: | + "# The CA key password is {{ easyrsa_CA_password }} #" + ssh_access: | + "# Shell access: ssh -i {{ ansible_ssh_private_key_file|default(omit) }} {{ ansible_ssh_user|default(omit) }}@{{ ansible_ssh_host|default(omit) }} #" SSH_keys: comment: algo@ssh diff --git a/deploy.yml b/deploy.yml index 94a6c3d1..75aac5ef 100644 --- a/deploy.yml +++ b/deploy.yml @@ -49,6 +49,10 @@ include: playbooks/common.yml tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] + - set_fact: + cloud_deployment: true + tags: ['cloud'] + roles: - { role: security, tags: [ 'security' ] } - { role: proxy, tags: [ 'proxy', 'adblock' ] } @@ -58,12 +62,14 @@ - { role: vpn, tags: [ 'vpn' ] } post_tasks: - - debug: msg="{{ congrats.split('\n') }}" + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" + - " {% if Store_CAKEY is defined and Store_CAKEY == 'N' %}{% else %}{{ congrats.ca_key_pass }}{% endif %}" + - " {% if cloud_deployment is defined %}{{ congrats.ssh_access }}{% endif %}" tags: always - - debug: msg="{{ additional_information.split('\n') }}" - tags: cloud - - name: Save the CA key password local_action: > shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password From b11015508ff173a7e1149cb9d0cc511eec733681 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 14 Feb 2017 20:42:12 +0300 Subject: [PATCH 295/769] Update README.md (#241) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 46bc80f9..041c4fbb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From 9a5801f43407af6c32e64d9425636812c2b2c152 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 15 Feb 2017 18:49:26 +0300 Subject: [PATCH 296/769] contgrats fix in update-users #243 --- users.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/users.yml b/users.yml index c9837a26..5958522b 100644 --- a/users.yml +++ b/users.yml @@ -202,7 +202,10 @@ with_items: "{{ users }}" post_tasks: - - debug: msg="{{ congrats.split('\n') }}" + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" tags: always handlers: From 7b468fae79817115e97d087cbaebf59daecaa60b Mon Sep 17 00:00:00 2001 From: Jacob Wilder Date: Thu, 16 Feb 2017 12:43:03 -0800 Subject: [PATCH 297/769] Fixed the azure role for situations where the user does not use a ~/.azure/credentials file (#242) --- roles/cloud-azure/tasks/main.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 14f34f26..53d54a6e 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -5,10 +5,10 @@ - name: Create a resource group azure_rm_resourcegroup: - secret: "{{ azure_secret | default(lookup('env','AZURE_CLIENT_ID')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_SECRET')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_TENANT')) }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" name: "{{ resource_group }}" location: "{{ region }}" tags: @@ -16,6 +16,10 @@ - name: Create a virtual network azure_rm_virtualnetwork: + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" resource_group: "{{ resource_group }}" name: algo_net address_prefixes: "10.10.0.0/16" @@ -24,6 +28,10 @@ - name: Create a subnet azure_rm_subnet: + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" resource_group: "{{ resource_group }}" name: algo_subnet address_prefix: "10.10.0.0/24" @@ -33,10 +41,10 @@ - name: Create an instance azure_rm_virtualmachine: - secret: "{{ azure_secret | default(lookup('env','AZURE_CLIENT_ID')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_SECRET')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_TENANT')) }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" resource_group: "{{ resource_group }}" admin_username: ubuntu virtual_network: algo_net From aca036142fb14c037ea457a72881580b757745a9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 17 Feb 2017 00:30:21 +0300 Subject: [PATCH 298/769] AndroidVPNClientProfiles #240 --- roles/vpn/tasks/main.yml | 12 ++++++++++++ roles/vpn/templates/sswan.j2 | 11 +++++++++++ 2 files changed, 23 insertions(+) create mode 100644 roles/vpn/templates/sswan.j2 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 16b0bf14..dfd31eb4 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -259,6 +259,18 @@ - "{{ PayloadContent.results }}" no_log: True +- name: Build the strongswan app android config + local_action: + module: template + src: sswan.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.sswan + mode: 0600 + become: no + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True + - name: Build the client ipsec config file local_action: module: template diff --git a/roles/vpn/templates/sswan.j2 b/roles/vpn/templates/sswan.j2 new file mode 100644 index 00000000..1c2a87a9 --- /dev/null +++ b/roles/vpn/templates/sswan.j2 @@ -0,0 +1,11 @@ +{ + "uuid": "{{ 600000 | random | to_uuid }}", + "name": "Algo VPN {{ IP_subject_alt_name }}", + "type": "ikev2-cert", + "remote": { + "addr": "{{ IP_subject_alt_name }}" + }, + "local": { + "p12": "{{ item.1.stdout }}" + } +} From 23d69da5286336fb46e209d7372c88721e3187db Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 20 Feb 2017 03:28:32 +0100 Subject: [PATCH 299/769] add warning about os security enhancements --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index d001a6e5..18e426bf 100755 --- a/algo +++ b/algo @@ -39,7 +39,7 @@ ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi read -p " -Do you want to apply operating system security enhancements on the server? +Do you want to apply operating system security enhancements on the server? (warning: replaces your sshd_config) [y/N]: " -r security_enabled security_enabled=${security_enabled:-n} if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi From d271b60b6a0e9fd7481a4c178d5162556b6bf56e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 20 Feb 2017 03:40:40 +0100 Subject: [PATCH 300/769] Update algo --- algo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/algo b/algo index 18e426bf..df83df20 100755 --- a/algo +++ b/algo @@ -293,11 +293,11 @@ Please choose the number of your zone. Press enter for default (#8) zone. non_cloud () { read -p " -Enter IP address of your server: (use localhost for local installation) +Enter the IP address of your server: (or use localhost for local installation) : " -r server_ip read -p " -What user should we use to login on the server? (ignore if you're deploying to localhost) +What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) [root]: " -r server_user server_user=${server_user:-root} @@ -340,11 +340,11 @@ Enter the number of your desired provider user_management () { read -p " -Enter IP address of your server: (use localhost for local installation) +Enter the IP address of your server: (or use localhost for local installation) : " -r server_ip read -p " -What user should we use to login on the server? (ignore if you're deploying to localhost) +What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) [root]: " -r server_user server_user=${server_user:-root} From e31f10da6da939a92b839b25d2205169ef5eecc9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 23 Feb 2017 18:25:46 +0300 Subject: [PATCH 301/769] Fixes #255 --- roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/vpn/tasks/main.yml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index b60a913e..e4563748 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -51,7 +51,7 @@ do_access_token: "{{ do_access_token }}" do_droplet_id: "{{ do.droplet.id }}" cloud_provider: digitalocean - ipv6_support: yes + ipv6_support: true - set_fact: cloud_instance_ip: "{{ do.droplet.ip_address }}" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index dfd31eb4..ca8ef886 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -46,9 +46,8 @@ - name: Configure ip6tables so IPSec traffic can traverse the tunnel iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - when: ((security_enabled is not defined) or - (security_enabled is defined and security_enabled != "y")) and - ipv6_support is defined and ipv6_support == "yes" + when: ((security_enabled is not defined) or (security_enabled is defined and security_enabled != "y")) and + (ipv6_support is defined and ipv6_support == true) notify: - save iptables From 2a7dd88a3c3e6ac8cbb33ad844f016e99ef932b8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 23 Feb 2017 18:44:30 +0300 Subject: [PATCH 302/769] Changed to ECDSA #102 --- roles/ssh_tunneling/tasks/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index ba0baf29..9ade7e33 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -31,8 +31,8 @@ createhome: yes generate_ssh_key: yes shell: /bin/false - ssh_key_type: rsa - ssh_key_bits: 2048 + ssh_key_type: ecdsa + ssh_key_bits: 256 ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" state: present @@ -41,7 +41,7 @@ - name: The authorized keys file created file: - src: '/var/jail/{{ item }}/.ssh/id_rsa.pub' + src: '/var/jail/{{ item }}/.ssh/id_ecdsa.pub' dest: '/var/jail/{{ item }}/.ssh/authorized_keys' owner: "{{ item }}" group: "{{ item }}" @@ -57,7 +57,7 @@ template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - name: Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" - name: Change mode for SSH private keys From b8f3d43eee28d25b30bd6d1afd2056634a18efe0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 23 Feb 2017 19:22:18 +0300 Subject: [PATCH 303/769] enable some additional debug info --- roles/cloud-digitalocean/tasks/main.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index e4563748..a4002bd7 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -14,7 +14,19 @@ until: ssh_keys.changed != true retries: 10 delay: 1 + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_access_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + - fail: msg: "Please, ensure that your API token is not read-only." From 43c2f5c31a56c0e8bee5f0765adf084c894873e0 Mon Sep 17 00:00:00 2001 From: Craig Date: Sat, 25 Feb 2017 10:07:32 -0800 Subject: [PATCH 304/769] Installs the recommended packages with strongswan, because we need the OpenSSL (#260) plugin from libstrongswan-standard-plugins for ECDH to work. --- roles/vpn/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index ca8ef886..f98b4c6b 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -17,7 +17,7 @@ when: Win10_Enabled is defined and Win10_Enabled == "Y" - name: Install StrongSwan - apt: name=strongswan state=latest update_cache=yes + apt: name=strongswan state=latest update_cache=yes install_recommends=yes - name: Enforcing ipsec with apparmor shell: aa-enforce "{{ item }}" From 79f66b7fda1638903417bdfd9225754afc31d0ab Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 25 Feb 2017 21:17:48 +0300 Subject: [PATCH 305/769] Update README.md. Fixes #259 `python-setuptools` is a recommended packages for which will be installed automatically for `python-pip` on a clean ubuntu 16.04 Updated README in order to avoid skipping the packages with `--no-install-recommends` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 041c4fbb..721e211b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` - Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` + Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) From 98558c43d264560e174841843410e1c9abc88547 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 26 Feb 2017 12:15:22 +0300 Subject: [PATCH 306/769] disable unneeded task --- users.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/users.yml b/users.yml index 5958522b..48d6d0e5 100644 --- a/users.yml +++ b/users.yml @@ -196,11 +196,6 @@ when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" with_items: "{{ valid_users.stdout_lines | default('null') }}" - - name: SSH | Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_rsa' dest=configs/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ users }}" - post_tasks: - debug: msg: From 8eb208c5b7154277ba6b2faa4d2575246dd9ab86 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 26 Feb 2017 20:17:12 +0300 Subject: [PATCH 307/769] enable ipv6 if the default gateway is defined. Fixes #244 --- roles/vpn/tasks/iptables.yml | 2 +- roles/vpn/tasks/main.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml index 0088a6d4..fc065c37 100644 --- a/roles/vpn/tasks/iptables.yml +++ b/roles/vpn/tasks/iptables.yml @@ -9,7 +9,7 @@ - name: Iptables configured template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 - when: ipv6_support is defined and ipv6_support == "yes" + when: ipv6_support is defined and ipv6_support == true with_items: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index f98b4c6b..9aec6045 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,6 +1,11 @@ - name: Gather Facts setup: +- name: Enable IPv6 + set_fact: + ipv6_support: true + when: ansible_default_ipv6.gateway is defined + - name: Generate password for the CA key shell: > < /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-12};echo; From c86ad1a2e5212032292a0eedd53f74ed71de4f03 Mon Sep 17 00:00:00 2001 From: nowfred Date: Sun, 26 Feb 2017 12:32:46 -0500 Subject: [PATCH 308/769] Added virtualenv information to README 'Deploy the Algo Server' section (#252) Fixes #222 --- .gitignore | 1 + README.md | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index e162478e..7235538d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ configs/* inventory_users *.kate-swp +env diff --git a/README.md b/README.md index 721e211b..4003b262 100644 --- a/README.md +++ b/README.md @@ -31,29 +31,39 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). 2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) -3. Install Algo's dependencies for your operating system. To do this, open a terminal and `cd` into the directory where you downloaded Algo, then: +3. Install Algo's core dependencies for your operating system. To do this, open a terminal and `cd` into the directory where you downloaded Algo, then: - macOS: `sudo easy_install pip && sudo pip install --ignore-install -r requirements.txt` - - Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev -y && sudo pip install -r requirements.txt` + macOS: `sudo easy_install pip` + + Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev -y` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) -4. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -5. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). +4. Configure and initialize a python virtual environment to manage Algo's python dependencies. Again from the directory where you have downloaded Algo, run: + + `virtualenv env && source env/bin/activate && pip install -r requirements.txt` + + Important: the virtual environment needs to be active whenever you are running Algo commands. This means that if you, for example, need to add or remove users, you must run + + `source env/bin/activate` + + first. + +5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. ``` - "\"#----------------------------------------------------------------------#\"", - "\"# Congratulations! #\"", - "\"# Your Algo server is running. #\"", - "\"# Config files and certificates are in the ./configs/ directory. #\"", - "\"# Go to https://whoer.net/ after connecting #\"", - "\"# and ensure that all your traffic passes through the VPN. #\"", - "\"# Local DNS resolver and Proxy IP address: 172.16.0.1 #\"", - "\"# The p12 and SSH keys password is XXXXXXXX #\"", - "\"#----------------------------------------------------------------------#\"", + "\"#----------------------------------------------------------------------#\"", + "\"# Congratulations! #\"", + "\"# Your Algo server is running. #\"", + "\"# Config files and certificates are in the ./configs/ directory. #\"", + "\"# Go to https://whoer.net/ after connecting #\"", + "\"# and ensure that all your traffic passes through the VPN. #\"", + "\"# Local DNS resolver and Proxy IP address: 172.16.0.1 #\"", + "\"# The p12 and SSH keys password is XXXXXXXX #\"", + "\"#----------------------------------------------------------------------#\"", ``` Note: Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. @@ -102,7 +112,7 @@ If you turned on the optional SSH tunneling role, then local user accounts will Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. - `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` + `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` ## Adding or Removing Users From 9d3a65b555f5dafadc8bc3a582487e54d5249848 Mon Sep 17 00:00:00 2001 From: Alex Van Camp Date: Sun, 26 Feb 2017 15:18:57 -0500 Subject: [PATCH 309/769] Android client setup documentation (#251). Fixes #240 * Android client setup documentation This is a first draft of Android Setup instructions. I expect that these will need some revising and that the formatting of this document will benefit from more tweaking. We may also want to elaborate on the contents of the "advanced settings" menu ([screenshot](https://i.imgur.com/smsmdQF.png)). * improve appearance on mobile devices --- README.md | 2 +- docs/Android Setup.md | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docs/Android Setup.md diff --git a/README.md b/README.md index 4003b262..6c38513e 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to ### Android Devices -You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. It's very simple to configure the StrongSwan VPN Client, just make a new profile with the IP address of your VPN server and choose which certificate to use. +You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/Android%20Setup.md) for more detailed steps. ### Windows diff --git a/docs/Android Setup.md b/docs/Android Setup.md new file mode 100644 index 00000000..ba42bdbb --- /dev/null +++ b/docs/Android Setup.md @@ -0,0 +1,38 @@ +**NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [StrongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. + +| Instruction | Screenshot(s) | +| ----------- | ---------- | +| 1. Copy your `{username}.p12` certificate to your phone's internal storage. | | +| 2. [Install the StrongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android) (Android 4+) | | +| 3. Open the app and tap "ADD VPN PROFILE" in the top right. | [![step3-thumb]][step3-screen] | +| 4. Enter the IP address or hostname of your Algo server and set the "VPN Type" to "IKEv2 Certificate". | [![step4-thumb]][step4-screen] | +| 5. Tap "Select user certificate". You will be shown a prompt, tap "INSTALL". | [![step5-thumb]][step5-screen] | +| 6. Use the "Open from" menu to select your certificate. If you downloaded your certificate to your phone, you may find that using the "Downloads" shortcut results in your `{username}.p12` certificate being grayed out. If this happens go back to the "Open from" menu and tap on the name of your phone. This will bring up the filesystem. From here, navigate to the folder where you saved your cert (such as "Downloads"), and try again. | [![step6-thumb]][step6-screen] | +| 7. Enter the password for your certificate. This password was printed to your console at the end of running the `algo` deployment script. Please note that in some cases, extracting the certificate can take several minutes. | [![step7-thumb]][step7-screen] | +| 8. Give your certificate a name (it will default to your Algo username), and ensure that "Credential use" is set to "VPN and apps". Tap "OK". | [![step8-thumb]][step8-screen] | +| 9. You'll then be brought to another prompt. Ensure your newly imported certificate is selected, and tap "ALLOW". Then, tap "SAVE" in the top right. | [![step9-thumb]][step9-screen] | +| 10. You will be returned to the main menu, and your newly-configured VPN profile should be listed. Tap the profile to connect. | [![step10-thumb]][step10-screen] | + +## Troubleshooting +### Tapping the VPN profile in StrongSwan has no effect. +Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before attempting to enable a StrongSwan VPN. If any other VPN is active, StrongSwan may silently fail to initialize a VPN connection. On Android 7, your can manage your VPNs by going to: Settings > Tap "More" under "Wireless & networks" > VPN > tap the gear icon next to any non-strongSwan VPNs listed and ensure they are disabled. + + +[step3-thumb]: https://i.imgur.com/LPwIGJE.png +[step4-thumb]: https://i.imgur.com/sFkDILg.png +[step5-thumb]: https://i.imgur.com/IliT5oD.png +[step6-thumb]: https://i.imgur.com/oghdCVp.png +[step7-thumb]: https://i.imgur.com/nDzJ7KS.png +[step8-thumb]: https://i.imgur.com/RPXSpCo.png +[step9-thumb]: https://i.imgur.com/uMinDPe.png +[step10-thumb]: https://i.imgur.com/hUEDjdo.png + + +[step3-screen]: https://i.imgur.com/xNMihCd.png +[step4-screen]: https://i.imgur.com/xYjoNNO.png +[step5-screen]: https://i.imgur.com/4qhKT1Z.png +[step6-screen]: https://i.imgur.com/MAaQuxH.png +[step7-screen]: https://i.imgur.com/aT2MPih.png +[step8-screen]: https://i.imgur.com/gvaKzkh.png +[step9-screen]: https://i.imgur.com/eZp8DNb.png +[step10-screen]: https://i.imgur.com/Nd8rYMJ.png \ No newline at end of file From 1cca3b1093a31e3b1391e8d9eccd4808623bbe5b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 28 Feb 2017 20:05:22 +0300 Subject: [PATCH 310/769] Ensure that ssh keys and configs are exist Fixes #250 (#254) --- algo | 7 +++++++ deploy.yml | 26 +++++++++----------------- playbooks/local_ssh.yml | 23 +++++++++++++++++++++++ playbooks/post.yml | 17 +++++++++++++++++ 4 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 playbooks/local_ssh.yml create mode 100644 playbooks/post.yml diff --git a/algo b/algo index df83df20..e5365bc8 100755 --- a/algo +++ b/algo @@ -308,6 +308,13 @@ Enter the public IP address of your server: (IMPORTANT! This IP is used to verif ROLES="local vpn" EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" SKIP_TAGS+=" cloud update-alternatives" + + read -p " +Was this server deployed by Algo previously? +[y/N]: " -r Deployed_By_Algo +Deployed_By_Algo=${Deployed_By_Algo:-n} +if [[ "$Deployed_By_Algo" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Deployed_By_Algo=Y"; fi + } algo_provisioning () { diff --git a/deploy.yml b/deploy.yml index 75aac5ef..184e7456 100644 --- a/deploy.yml +++ b/deploy.yml @@ -9,6 +9,12 @@ include: playbooks/local.yml tags: [ 'always' ] + - name: Local pre-tasks + include: playbooks/local_ssh.yml + become: false + when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" + tags: [ 'local' ] + roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } - { role: cloud-ec2, tags: ['ec2'] } @@ -17,24 +23,10 @@ - { role: local, tags: ['local'] } post_tasks: - - name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ cloud_instance_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present + - name: Local pre-tasks + include: playbooks/post.yml become: false - tags: - - cloud - - - name: A short pause, in order to be sure the instance is ready - pause: - seconds: 10 - tags: - - cloud + tags: [ 'cloud' ] - name: Configure the server and install required software hosts: vpn-host diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml new file mode 100644 index 00000000..cf5c1751 --- /dev/null +++ b/playbooks/local_ssh.yml @@ -0,0 +1,23 @@ +--- + +- name: Ensure the local ssh directory is exist + local_action: + module: file + path: "~/.ssh/" + state: directory + +- name: Copy the algo ssh key to the local ssh directory + local_action: + module: copy + src: configs/algo.pem + dest: ~/.ssh/algo.pem + mode: '0600' + +- name: Configure the local ssh config + blockinfile: + dest: "~/.ssh/config" + marker: "# {mark} ALGO MANAGED BLOCK {{ cloud_instance_ip|default(server_ip) }}" + insertbefore: BOF + block: | + Host {{ cloud_instance_ip|default(server_ip) }} + IdentityFile ~/.ssh/algo.pem diff --git a/playbooks/post.yml b/playbooks/post.yml new file mode 100644 index 00000000..b51d0ca4 --- /dev/null +++ b/playbooks/post.yml @@ -0,0 +1,17 @@ +--- + +- name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ cloud_instance_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present + +- name: A short pause, in order to be sure the instance is ready + pause: + seconds: 10 + +- include: local_ssh.yml From d7d976784cd888d216cf9c8ebe5695cd43e44b9b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 28 Feb 2017 21:34:28 +0300 Subject: [PATCH 311/769] Fixes #207 --- roles/ssh_tunneling/tasks/main.yml | 14 ++++++++++++-- roles/ssh_tunneling/templates/ssh_config.j2 | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 roles/ssh_tunneling/templates/ssh_config.j2 diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 9ade7e33..2c667ac0 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -57,13 +57,23 @@ template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - name: Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem flat=yes + fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" - name: Change mode for SSH private keys - local_action: file path=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_{{ item }}.ssh.pem mode=0600 + local_action: file path=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem mode=0600 with_items: "{{ users }}" become: false - name: Fetch the known_hosts file fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_known_hosts flat=yes + +- name: Build the client ssh config + local_action: + module: template + src: ssh_config.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config + mode: 0600 + become: no + with_items: + - "{{ users }}" diff --git a/roles/ssh_tunneling/templates/ssh_config.j2 b/roles/ssh_tunneling/templates/ssh_config.j2 new file mode 100644 index 00000000..04931fc2 --- /dev/null +++ b/roles/ssh_tunneling/templates/ssh_config.j2 @@ -0,0 +1,7 @@ +Host algo + DynamicForward 127.0.0.1:1080 + LogLevel quiet + Compression yes + IdentityFile {{ item }}.ssh.pem + User {{ item }} + Hostname {{ IP_subject_alt_name }} From eba04b3c91eb5135e241d5a5f163d3ce8da0aa5a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 28 Feb 2017 22:22:06 +0300 Subject: [PATCH 312/769] ssh_config fix --- playbooks/local_ssh.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml index cf5c1751..20b8886d 100644 --- a/playbooks/local_ssh.yml +++ b/playbooks/local_ssh.yml @@ -18,6 +18,7 @@ dest: "~/.ssh/config" marker: "# {mark} ALGO MANAGED BLOCK {{ cloud_instance_ip|default(server_ip) }}" insertbefore: BOF + create: yes block: | Host {{ cloud_instance_ip|default(server_ip) }} IdentityFile ~/.ssh/algo.pem From 71b3b5ac46f5db122b5a11d842bf19ff0e400c56 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 28 Feb 2017 22:29:21 +0300 Subject: [PATCH 313/769] Install from Windows #193 --- README.md | 4 +++- docs/WINDOWS.md | 29 +++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 docs/WINDOWS.md diff --git a/README.md b/README.md index 6c38513e..4d42c6b4 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,12 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua macOS: `sudo easy_install pip` - Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev -y` + Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv -y` Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) + Windows: See the [Windows documentation](docs/WINDOWS.md) + 4. Configure and initialize a python virtual environment to manage Algo's python dependencies. Again from the directory where you have downloaded Algo, run: `virtualenv env && source env/bin/activate && pip install -r requirements.txt` diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md new file mode 100644 index 00000000..f6c3597d --- /dev/null +++ b/docs/WINDOWS.md @@ -0,0 +1,29 @@ +# Windows + +## How to run Algo on Windows 10 + +Before run Algo, you have to have: + +* Windows 10 (Anniversary update or later version) +* 64-bit installation (can't run on 32-bit systems) + +Once you verify your system is 64-bit and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': +1. Open 'Settings' +2. Click 'Update & Security', then click the 'For developers' option on the left. +3. Toggle the 'Developer mode' option, and accept any warnings Windows pops up. + +Wait a minute for Windows to install a few things in the background (it will eventually let you know a restart may be required for changes to take effect—ignore that for now). Next, to install the actual Linux Subsystem, you have to jump over to 'Control Panel', and do the following: + +1. Click on 'Programs' +2. Click on 'Turn Windows features on or off' +3. Scroll down and check 'Windows Subsystem for Linux (Beta)', and then click OK. + +The subsystem will be installed, then Windows will require a reboot. Reboot, then open up the start menu and enter 'bash' (to open up 'Bash' installation in a new command prompt). Fill out all the questions (it will have you create a separate user account for the Linux subsystem), and once that's all done (it takes a few minutes to install), you will finally have Ubuntu running on your Windows laptop, somewhat integrated with Windows. + +Install additional packages: +`sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv git -y` + +Clone the Algo repository: +`https://github.com/trailofbits/algo && cd algo` + +Now, you can go through the [README](https://github.com/trailofbits/algo#deploy-the-algo-server) (start from the 4th step) and deploy your Algo server! diff --git a/requirements.txt b/requirements.txt index dac22242..6dedf8d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +setuptools>=11.3 ansible>=2.1,<2.2.1 dopy==0.3.5 boto>=2.5 From 95c42002f29f20f54abb5f51ccf9d70f9439f2f5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 28 Feb 2017 22:30:43 +0300 Subject: [PATCH 314/769] Update WINDOWS.md --- docs/WINDOWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index f6c3597d..3a53f7e2 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -8,6 +8,7 @@ Before run Algo, you have to have: * 64-bit installation (can't run on 32-bit systems) Once you verify your system is 64-bit and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': + 1. Open 'Settings' 2. Click 'Update & Security', then click the 'For developers' option on the left. 3. Toggle the 'Developer mode' option, and accept any warnings Windows pops up. @@ -21,9 +22,11 @@ Wait a minute for Windows to install a few things in the background (it will eve The subsystem will be installed, then Windows will require a reboot. Reboot, then open up the start menu and enter 'bash' (to open up 'Bash' installation in a new command prompt). Fill out all the questions (it will have you create a separate user account for the Linux subsystem), and once that's all done (it takes a few minutes to install), you will finally have Ubuntu running on your Windows laptop, somewhat integrated with Windows. Install additional packages: + `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv git -y` Clone the Algo repository: + `https://github.com/trailofbits/algo && cd algo` Now, you can go through the [README](https://github.com/trailofbits/algo#deploy-the-algo-server) (start from the 4th step) and deploy your Algo server! From 0bf3e809a40b138018014df98f3ff8c56fdfdac5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 3 Mar 2017 20:46:11 +0300 Subject: [PATCH 315/769] Linux clients installation vpn #44 --- deploy_client.yml | 31 ++++++++++++ roles/client/handlers/main.yml | 4 ++ roles/client/tasks/main.yml | 73 +++++++++++++++++++++++++++ roles/client/tasks/systems/CentOS.yml | 6 +++ roles/client/tasks/systems/Debian.yml | 5 ++ roles/client/tasks/systems/Ubuntu.yml | 5 ++ roles/client/tasks/systems/main.yml | 10 ++++ 7 files changed, 134 insertions(+) create mode 100644 deploy_client.yml create mode 100644 roles/client/handlers/main.yml create mode 100644 roles/client/tasks/main.yml create mode 100644 roles/client/tasks/systems/CentOS.yml create mode 100644 roles/client/tasks/systems/Debian.yml create mode 100644 roles/client/tasks/systems/Ubuntu.yml create mode 100644 roles/client/tasks/systems/main.yml diff --git a/deploy_client.yml b/deploy_client.yml new file mode 100644 index 00000000..67b81368 --- /dev/null +++ b/deploy_client.yml @@ -0,0 +1,31 @@ +- name: Configure the client + hosts: localhost + tasks: + - name: Add the droplet to an inventory group + add_host: + name: "{{ client_ip }}" + groups: client-host + ansible_ssh_user: "{{ server_user }}" + +- name: Configure the client and install required software + hosts: client-host + gather_facts: false + become: true + vars_files: + - config.cfg + + pre_tasks: + - name: Get the OS + raw: uname -a + register: distribution + + - name: Ubuntu Xenial | Install prerequisites + raw: > + test -x /usr/bin/python2.7 || + sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 && + sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + changed_when: false + when: "'ubuntu' in distribution.stdout" + + roles: + - { role: client, tags: ['client'] } diff --git a/roles/client/handlers/main.yml b/roles/client/handlers/main.yml new file mode 100644 index 00000000..84c893a1 --- /dev/null +++ b/roles/client/handlers/main.yml @@ -0,0 +1,4 @@ +--- + +- name: restart strongswan + service: name=strongswan state=restarted diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml new file mode 100644 index 00000000..8bd57e88 --- /dev/null +++ b/roles/client/tasks/main.yml @@ -0,0 +1,73 @@ +- name: Gather Facts + setup: + +- name: Include system based facts and tasks + include: systems/main.yml + +- name: Cheking the signature algorithm + local_action: > + shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 + become: no + register: sig_algo + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + +- name: Change the algorithm to RSA + set_fact: + Win10_Enabled: "Y" + when: '"ecdsa" not in sig_algo.stdout' + +- name: Install prerequisites + package: name="{{ item }}" state=present + with_items: + - "{{ prerequisites }}" + +- name: Install StrongSwan + package: name=strongswan state=present + +- name: Setup the ipsec config + template: + src: "roles/vpn/templates/client_ipsec.conf.j2" + dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.conf" + mode: '0644' + with_items: + - "{{ user }}" + notify: + - restart strongswan + +- name: Setup the ipsec secrets + template: + src: "roles/vpn/templates/client_ipsec.secrets.j2" + dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.secrets" + mode: '0600' + with_items: + - "{{ user }}" + notify: + - restart strongswan + +- name: Include additional ipsec config + lineinfile: + dest: "{{ item.dest }}" + line: "{{ item.line }}" + create: yes + with_items: + - dest: "{{ configs_prefix }}/ipsec.conf" + line: "include ipsec.*.conf" + - dest: "{{ configs_prefix }}/ipsec.secrets" + line: "include ipsec.*.secrets" + notify: + - restart strongswan + +- name: Setup the certificates and keys + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + with_items: + - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ user }}.crt" + dest: "{{ configs_prefix }}/ipsec.d/certs/{{ IP_subject_alt_name }}_{{ user }}.crt" + - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + dest: "{{ configs_prefix }}/ipsec.d/cacerts/{{ IP_subject_alt_name }}.pem" + - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ user }}.key" + dest: "{{ configs_prefix }}/ipsec.d/private/{{ IP_subject_alt_name }}_{{ user }}.key" + notify: + - restart strongswan diff --git a/roles/client/tasks/systems/CentOS.yml b/roles/client/tasks/systems/CentOS.yml new file mode 100644 index 00000000..60df753f --- /dev/null +++ b/roles/client/tasks/systems/CentOS.yml @@ -0,0 +1,6 @@ +--- + +- set_fact: + prerequisites: + - epel-release + configs_prefix: /etc/strongswan/ diff --git a/roles/client/tasks/systems/Debian.yml b/roles/client/tasks/systems/Debian.yml new file mode 100644 index 00000000..9e5461d2 --- /dev/null +++ b/roles/client/tasks/systems/Debian.yml @@ -0,0 +1,5 @@ +--- + +- set_fact: + prerequisites: [] + configs_prefix: /etc/ diff --git a/roles/client/tasks/systems/Ubuntu.yml b/roles/client/tasks/systems/Ubuntu.yml new file mode 100644 index 00000000..9e5461d2 --- /dev/null +++ b/roles/client/tasks/systems/Ubuntu.yml @@ -0,0 +1,5 @@ +--- + +- set_fact: + prerequisites: [] + configs_prefix: /etc/ diff --git a/roles/client/tasks/systems/main.yml b/roles/client/tasks/systems/main.yml new file mode 100644 index 00000000..277b426b --- /dev/null +++ b/roles/client/tasks/systems/main.yml @@ -0,0 +1,10 @@ +--- + +- include: Debian.yml + when: ansible_distribution == 'Debian' + +- include: Ubuntu.yml + when: ansible_distribution == 'Ubuntu' + +- include: CentOS.yml + when: ansible_distribution == 'CentOS' From 2a4d1837b5f80e986400e4e5afa712b5b58c39c2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 4 Mar 2017 23:05:02 +0300 Subject: [PATCH 316/769] Some fixes. Fedora client. Close #44 --- deploy.yml | 2 +- deploy_client.yml | 22 ++++++++++++++++++++-- docs/CLIENT.md | 17 +++++++++++++++++ roles/client/tasks/main.yml | 12 ++++++------ roles/client/tasks/systems/Fedora.yml | 6 ++++++ roles/client/tasks/systems/main.yml | 3 +++ 6 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 docs/CLIENT.md create mode 100644 roles/client/tasks/systems/Fedora.yml diff --git a/deploy.yml b/deploy.yml index 184e7456..4f967b7c 100644 --- a/deploy.yml +++ b/deploy.yml @@ -13,7 +13,7 @@ include: playbooks/local_ssh.yml become: false when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" - tags: [ 'local' ] + tags: [ 'local' ] roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } diff --git a/deploy_client.yml b/deploy_client.yml index 67b81368..baf26c81 100644 --- a/deploy_client.yml +++ b/deploy_client.yml @@ -1,11 +1,16 @@ - name: Configure the client hosts: localhost + vars_files: + - config.cfg + tasks: - name: Add the droplet to an inventory group add_host: name: "{{ client_ip }}" groups: client-host - ansible_ssh_user: "{{ server_user }}" + ansible_ssh_user: "{{ server_ssh_user }}" + vpn_user: "{{ vpn_user }}" + server_ip: "{{ server_ip }}" - name: Configure the client and install required software hosts: client-host @@ -19,7 +24,11 @@ raw: uname -a register: distribution - - name: Ubuntu Xenial | Install prerequisites + - name: Modify the server name fact + set_fact: + IP_subject_alt_name: "{{ server_ip }}" + + - name: Ubuntu Xenial | Install prerequisites raw: > test -x /usr/bin/python2.7 || sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 && @@ -27,5 +36,14 @@ changed_when: false when: "'ubuntu' in distribution.stdout" + - name: Fedora 25 | Install prerequisites + raw: > + test -x /usr/bin/python2.7 || + sudo dnf install python2 -y && + sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && + rpm -ql python2-dnf || dnf install python2-dnf -y + changed_when: false + when: "'fedora' in distribution.stdout" + roles: - { role: client, tags: ['client'] } diff --git a/docs/CLIENT.md b/docs/CLIENT.md new file mode 100644 index 00000000..af9dcbd9 --- /dev/null +++ b/docs/CLIENT.md @@ -0,0 +1,17 @@ +### Client installation + +It's possible to deploy an ipsec connection on Linux clients. +Supported distributives are: Debian, Ubuntu, CentOS, Fedora + +The playbook is `deploy_client.yml` + +Required variables: + +* client_ip - the IP address of your client machine (You can use `localhost` in order to deploy locally) +* vpn_user - the username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) +* client_ssh_user - the username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) +* server_ip - the vpn server ip address + +Example: + +`ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root'` diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 8bd57e88..c5b69971 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -31,7 +31,7 @@ dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.conf" mode: '0644' with_items: - - "{{ user }}" + - "{{ vpn_user }}" notify: - restart strongswan @@ -41,7 +41,7 @@ dest: "{{ configs_prefix }}/ipsec.{{ IP_subject_alt_name }}.secrets" mode: '0600' with_items: - - "{{ user }}" + - "{{ vpn_user }}" notify: - restart strongswan @@ -63,11 +63,11 @@ src: "{{ item.src }}" dest: "{{ item.dest }}" with_items: - - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ user }}.crt" - dest: "{{ configs_prefix }}/ipsec.d/certs/{{ IP_subject_alt_name }}_{{ user }}.crt" + - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ vpn_user }}.crt" + dest: "{{ configs_prefix }}/ipsec.d/certs/{{ IP_subject_alt_name }}_{{ vpn_user }}.crt" - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" dest: "{{ configs_prefix }}/ipsec.d/cacerts/{{ IP_subject_alt_name }}.pem" - - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ user }}.key" - dest: "{{ configs_prefix }}/ipsec.d/private/{{ IP_subject_alt_name }}_{{ user }}.key" + - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ vpn_user }}.key" + dest: "{{ configs_prefix }}/ipsec.d/private/{{ IP_subject_alt_name }}_{{ vpn_user }}.key" notify: - restart strongswan diff --git a/roles/client/tasks/systems/Fedora.yml b/roles/client/tasks/systems/Fedora.yml new file mode 100644 index 00000000..ec920927 --- /dev/null +++ b/roles/client/tasks/systems/Fedora.yml @@ -0,0 +1,6 @@ +--- + +- set_fact: + prerequisites: + - libselinux-python + configs_prefix: /etc/strongswan/ diff --git a/roles/client/tasks/systems/main.yml b/roles/client/tasks/systems/main.yml index 277b426b..85da1ebd 100644 --- a/roles/client/tasks/systems/main.yml +++ b/roles/client/tasks/systems/main.yml @@ -8,3 +8,6 @@ - include: CentOS.yml when: ansible_distribution == 'CentOS' + +- include: Fedora.yml + when: ansible_distribution == 'Fedora' From 7cde31e50fcb5745d131791b6b3c2748d2e1f90b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 4 Mar 2017 23:06:30 +0300 Subject: [PATCH 317/769] Update CLIENT.md --- docs/CLIENT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/CLIENT.md b/docs/CLIENT.md index af9dcbd9..a44d4be8 100644 --- a/docs/CLIENT.md +++ b/docs/CLIENT.md @@ -1,17 +1,17 @@ -### Client installation +# Client installation It's possible to deploy an ipsec connection on Linux clients. Supported distributives are: Debian, Ubuntu, CentOS, Fedora The playbook is `deploy_client.yml` -Required variables: +### Required variables: * client_ip - the IP address of your client machine (You can use `localhost` in order to deploy locally) * vpn_user - the username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) * client_ssh_user - the username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) * server_ip - the vpn server ip address -Example: +### Example: `ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root'` From a49caa3c3109c16759131eed82499f37012c0abe Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 4 Mar 2017 23:08:31 +0300 Subject: [PATCH 318/769] Update CLIENT.md --- docs/CLIENT.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CLIENT.md b/docs/CLIENT.md index a44d4be8..41d0f96d 100644 --- a/docs/CLIENT.md +++ b/docs/CLIENT.md @@ -7,10 +7,10 @@ The playbook is `deploy_client.yml` ### Required variables: -* client_ip - the IP address of your client machine (You can use `localhost` in order to deploy locally) -* vpn_user - the username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) -* client_ssh_user - the username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) -* server_ip - the vpn server ip address +* `client_ip` - The IP address of your client machine (You can use `localhost` in order to deploy locally) +* `vpn_user` - The username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) +* `client_ssh_user` - The username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) +* `server_ip` - The vpn server ip address ### Example: From 69063300470793a965ab2d33b850de7368a9975c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 5 Mar 2017 10:42:26 +0300 Subject: [PATCH 319/769] Update README.md. Fixes #265 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d42c6b4..0e7439b1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Configure and initialize a python virtual environment to manage Algo's python dependencies. Again from the directory where you have downloaded Algo, run: - `virtualenv env && source env/bin/activate && pip install -r requirements.txt` + `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` Important: the virtual environment needs to be active whenever you are running Algo commands. This means that if you, for example, need to add or remove users, you must run From 237fcc7a7f5ee04fdb0cfe137786c7a55c158de2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 5 Mar 2017 10:58:42 +0300 Subject: [PATCH 320/769] additional variables --- roles/vpn/tasks/main.yml | 2 +- users.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 9aec6045..567614cd 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -12,7 +12,7 @@ register: CA_password - set_fact: - easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" easyrsa_CA_password: "{{ CA_password.stdout }}" IP_subject_alt_name: "{{ IP_subject_alt_name }}" diff --git a/users.yml b/users.yml index 48d6d0e5..105c9be8 100644 --- a/users.yml +++ b/users.yml @@ -38,7 +38,7 @@ pre_tasks: - set_fact: IP_subject_alt_name: "{{ IP_subject }}" - easyrsa_p12_export_password: "{{ (ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0] }}" + easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" roles: - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } From 5cbf1252027522df7a3995217d4604db2aa4859c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 5 Mar 2017 21:33:01 +0300 Subject: [PATCH 321/769] Some refactoring. Disable unneeded variables. --- algo | 3 ++- config.cfg | 32 ++----------------------------- roles/cloud-ec2/defaults/main.yml | 5 +++++ roles/vpn/defaults/main.yml | 21 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 roles/cloud-ec2/defaults/main.yml create mode 100644 roles/vpn/defaults/main.yml diff --git a/algo b/algo index e5365bc8..20d27789 100755 --- a/algo +++ b/algo @@ -303,7 +303,8 @@ What user should we use to login on the server? (note: passwordless login requir read -p " Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -: " -r IP_subject +[$server_ip]: " -r IP_subject + IP_subject=${IP_subject:-$server_ip} ROLES="local vpn" EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" diff --git a/config.cfg b/config.cfg index 9ac69db6..d48872da 100644 --- a/config.cfg +++ b/config.cfg @@ -11,16 +11,13 @@ auditd_action_mail_acct: email@example.com ### Advanced users only below this line ### -easyrsa_dir: /opt/easy-rsa-ipsec -easyrsa_ca_expire: 3650 -easyrsa_cert_expire: 3650 - # If True re-init all existing certificates. (True or False) easyrsa_reinit_existent: False vpn_network: 10.19.48.0/24 -vpn_network_ipv6: 'fd9d:bc11:4020::/48' # https://www.sixxs.net/tools/whois/?fd9d:bc11:4020::/48 +vpn_network_ipv6: 'fd9d:bc11:4020::/48' + server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" @@ -32,30 +29,6 @@ dns_servers: - 2001:4860:4860::8888 - 2001:4860:4860::8844 -strongswan_enabled_plugins: - - aes - - gcm - - hmac - - kernel-netlink - - nonce - - openssl - - pem - - pgp - - pkcs12 - - pkcs7 - - pkcs8 - - pubkey - - random - - revocation - - sha2 - - socket-default - - stroke - - x509 - -ec2_vpc_nets: - cidr_block: 172.251.0.0/23 - subnet_cidr: 172.251.1.0/24 - # IP address for the proxy and the local dns resolver local_service_ip: 172.16.0.1 @@ -64,7 +37,6 @@ VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" # Block traffic between connected clients - BetweenClients_DROP: Y congrats: diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml new file mode 100644 index 00000000..173d9696 --- /dev/null +++ b/roles/cloud-ec2/defaults/main.yml @@ -0,0 +1,5 @@ +--- + +ec2_vpc_nets: + cidr_block: 172.251.0.0/23 + subnet_cidr: 172.251.1.0/24 diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml new file mode 100644 index 00000000..cc3ee72a --- /dev/null +++ b/roles/vpn/defaults/main.yml @@ -0,0 +1,21 @@ +--- + +strongswan_enabled_plugins: + - aes + - gcm + - hmac + - kernel-netlink + - nonce + - openssl + - pem + - pgp + - pkcs12 + - pkcs7 + - pkcs8 + - pubkey + - random + - revocation + - sha2 + - socket-default + - stroke + - x509 From f7da2e3888a882d2fd2e51883d48c05627f00a21 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 5 Mar 2017 23:19:15 +0300 Subject: [PATCH 322/769] EC2 dynamic enventory. Fixes #73 --- config.cfg | 7 +++++++ playbooks/local.yml | 11 +++++++++++ roles/cloud-ec2/tasks/main.yml | 27 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/config.cfg b/config.cfg index d48872da..a8c86e86 100644 --- a/config.cfg +++ b/config.cfg @@ -58,3 +58,10 @@ SSH_keys: comment: algo@ssh private: configs/algo.pem public: configs/algo.pem.pub + +dynamic_inventory_groups: + - azure + - digitalocean + - ec2 + - gce + - local diff --git a/playbooks/local.yml b/playbooks/local.yml index a7bc353e..76274012 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -12,3 +12,14 @@ - name: Change mode for the SSH private key local_action: file path=configs/algo.pem mode=0600 + +- name: Ensure the dynamic inventory exists + blockinfile: + dest: configs/inventory.dynamic + marker: "# {mark} ALGO MANAGED BLOCK" + create: yes + block: | + [algo:children] + {% for group in dynamic_inventory_groups %} + {{ group }} + {% endfor %} diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index edbfc025..00a87dd6 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -96,6 +96,7 @@ region: "{{ region }}" instance_tags: name: "{{ aws_server_name }}" + Environment: Algo exact_count: 1 count_tag: name: "{{ aws_server_name }}" @@ -115,3 +116,29 @@ - set_fact: cloud_instance_ip: "{{ ec2.tagged_instances[0].public_ip }}" + +- name: Get EC2 instances + ec2_remote_facts: + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + region: "{{ region }}" + filters: + instance-state-name: running + "tag:Environment": Algo + register: algo_instances + +- name: Ensure the group ec2 exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[ec2]' + +- name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[ec2\]' + regexp: "^{{ item.public_ip_address }}.*" + line: "{{ item.public_ip_address }}" + with_items: + - "{{ algo_instances.instances }}" From dfb1cbc28290ee3b98d849b85586c645d22b6c5d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 5 Mar 2017 23:38:15 +0300 Subject: [PATCH 323/769] DigitalOcean dynamic inventory --- roles/cloud-digitalocean/tasks/main.yml | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index a4002bd7..b91d19b1 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -67,3 +67,36 @@ - set_fact: cloud_instance_ip: "{{ do.droplet.ip_address }}" + +- name: Tag the groplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ do_access_token }}" + state: present + +- name: Get droplets + uri: + url: "https://api.digitalocean.com/v2/droplets?tag_name=Environment:Algo" + method: GET + status_code: 200 + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ do_access_token }}" + register: do_droplets + +- name: Ensure the group ec2 exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[digitalocean]' + +- name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[digitalocean\]' + regexp: "^{{ item.networks.v4[0].ip_address }}.*" + line: "{{ item.networks.v4[0].ip_address }}" + with_items: + - "{{ do_droplets.json.droplets }}" From 69ff22f9bbc6fab7b97f929a4ebeeef5ce97f07c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 00:55:13 +0300 Subject: [PATCH 324/769] fix typo --- roles/cloud-digitalocean/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index b91d19b1..41ca4a11 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -85,11 +85,11 @@ Authorization: "Bearer {{ do_access_token }}" register: do_droplets -- name: Ensure the group ec2 exists in the dynamic inventory file +- name: Ensure the group digitalocean exists in the dynamic inventory file lineinfile: state: present dest: configs/inventory.dynamic - line: '[digitalocean]' + line: '[digitalocean]' - name: Populate the dynamic inventory lineinfile: From 9cc9cf7b5fdbc8eed4dd6a3080f5cbc2030da4e1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 00:55:54 +0300 Subject: [PATCH 325/769] local inventory #30 --- roles/local/tasks/main.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index d2deff6f..9b34d7c6 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -19,3 +19,17 @@ - set_fact: cloud_instance_ip: "{{ server_ip }}" + +- name: Ensure the group local exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[local]' + +- name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[local\]' + regexp: "^{{ server_ip }}.*" + line: "{{ server_ip }}" From 6e538627db08bd743f64bb0efc1d431506482b58 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 00:56:15 +0300 Subject: [PATCH 326/769] gce inventory #30 --- roles/cloud-gce/tasks/main.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index e94d0d55..6464cba8 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -12,6 +12,8 @@ credentials_file: "{{ credentials_file }}" project_id: "{{ credentials_file_lookup.project_id }}" metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' + tags: + - "environment: algo" register: google_vm - name: Add the instance to an inventory group @@ -38,3 +40,17 @@ - set_fact: cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" + +- name: Ensure the group gce exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[gce]' + +- name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[gce\]' + regexp: "^{{ google_vm.instance_data[0].public_ip }}.*" + line: "{{ google_vm.instance_data[0].public_ip }}" From c6f5bcc5f2cbd932b69b143bcf99e797aa70933d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 19:03:57 +0300 Subject: [PATCH 327/769] Create AZURE.md --- docs/AZURE.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/AZURE.md diff --git a/docs/AZURE.md b/docs/AZURE.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/docs/AZURE.md @@ -0,0 +1 @@ + From 9b017a36705d01b224c7e28c01f53711d1d072af Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 20:35:02 +0300 Subject: [PATCH 328/769] Azure: How to create app credentials #261 --- docs/AZURE.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/AZURE.md b/docs/AZURE.md index 8b137891..91197af7 100644 --- a/docs/AZURE.md +++ b/docs/AZURE.md @@ -1 +1,56 @@ +| Instruction | Screenshot(s) | +| ----------- | ---------- | +| 1. Go to https://portal.azure.com/ | | +| 2. Go to **Azure Active Directory** | [![step2-thumb]][step2-screen] | +| 3. Go to **App registrations** and click to **Add** | [![step3-thumb]][step3-screen] | +| 4. Fill out the forms and click **Create** | [![step4-thumb]][step4-screen] | +| 5. Click on the app name | [![step5-thumb]][step5-screen] | +| 6. Copy and save somewhere the **Application ID** and click on **Keys**. | [![step6-thumb]][step6-screen] | +| 7. Fill out the forms and click **Save**. Copy and save somewhere the **Secret ID** (the value) | [![step7-thumb]][step7-screen] | +| 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | +| 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | +| 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | +| 11. Select a role (Contibutor will enough for all)| [![step11-thumb]][step11-screen] | +| 12. Swith next to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | +Now you can use Environment Variables: + +* AZURE_CLIENT_ID - from the 6th step +* AZURE_SECRET - from the 7th step +* AZURE_TENANT - from the 8th step +* AZURE_SUBSCRIPTION_ID - from the 9th step + +or create the credentials file ~/.azure/credentials: + +``` +[default] +client_id= +secret= +tenant= +subscription_id= +``` +or just pass those values to the Algo script + +[step2-screen]: http://i.imgur.com/ENvSupE.png +[step3-screen]: http://i.imgur.com/sPLQaQe.jpg +[step4-screen]: http://i.imgur.com/di3xFCM.jpg +[step5-screen]: http://i.imgur.com/SipQyRA.jpg +[step6-screen]: http://i.imgur.com/RRTqV7C.jpg +[step7-screen]: http://i.imgur.com/ZnqJeVv.jpg +[step8-screen]: http://i.imgur.com/WAS8Ovl.png +[step9-screen]: http://i.imgur.com/IvTN7o1.jpg +[step10-screen]: http://i.imgur.com/j6dgo75.png +[step11-screen]: http://i.imgur.com/NUJ6k7i.jpg +[step12-screen]: http://i.imgur.com/VZv5qwb.jpg + +[step2-thumb]: http://i.imgur.com/ENvSupEm.png +[step3-thumb]: http://i.imgur.com/sPLQaQem.jpg +[step4-thumb]: http://i.imgur.com/di3xFCMm.jpg +[step5-thumb]: http://i.imgur.com/SipQyRAm.jpg +[step6-thumb]: http://i.imgur.com/RRTqV7Cm.jpg +[step7-thumb]: http://i.imgur.com/ZnqJeVvm.jpg +[step8-thumb]: http://i.imgur.com/WAS8Ovlm.png +[step9-thumb]: http://i.imgur.com/IvTN7o1m.jpg +[step10-thumb]: http://i.imgur.com/j6dgo75m.png +[step11-thumb]: http://i.imgur.com/NUJ6k7im.jpg +[step12-thumb]: http://i.imgur.com/VZv5qwbm.jpg From adffe60342d3c254a2c99efc6cfc3eb7d76fe8a9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 21:23:17 +0300 Subject: [PATCH 329/769] Update AZURE.md --- docs/AZURE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/AZURE.md b/docs/AZURE.md index 91197af7..a38278af 100644 --- a/docs/AZURE.md +++ b/docs/AZURE.md @@ -1,3 +1,5 @@ +### Authenticating with Azure + | Instruction | Screenshot(s) | | ----------- | ---------- | | 1. Go to https://portal.azure.com/ | | From 07ebb4bf70c2da94ccb500792b7a4643c1796f85 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 21:23:58 +0300 Subject: [PATCH 330/769] Additional requirements. Needed for MacOS and azure --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6dedf8d0..d5799fb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ dopy==0.3.5 boto>=2.5 boto3 azure==2.0.0rc5 +msrest==0.4.1 apache-libcloud six pyopenssl From 0d1731e05839b6484773e1fbe2a11327a4d3e12f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 21:24:48 +0300 Subject: [PATCH 331/769] update tags for azure resources --- roles/cloud-azure/tasks/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 53d54a6e..6a89a3b1 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -12,7 +12,7 @@ name: "{{ resource_group }}" location: "{{ region }}" tags: - service: algo + Environment: Algo - name: Create a virtual network azure_rm_virtualnetwork: @@ -24,7 +24,7 @@ name: algo_net address_prefixes: "10.10.0.0/16" tags: - service: algo + Environment: Algo - name: Create a subnet azure_rm_subnet: @@ -37,7 +37,7 @@ address_prefix: "10.10.0.0/24" virtual_network: algo_net tags: - service: algo + Environment: Algo - name: Create an instance azure_rm_virtualmachine: @@ -52,7 +52,7 @@ ssh_password_enabled: false vm_size: Standard_D1 tags: - service: algo + Environment: Algo ssh_public_keys: - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } image: From 8280201dc197e71cf01dbb06011b9c28d17956ad Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 21:33:55 +0300 Subject: [PATCH 332/769] Update azure prompts. Fixes #261 --- algo | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/algo b/algo index 20d27789..d82bad85 100755 --- a/algo +++ b/algo @@ -66,29 +66,28 @@ deploy () { azure () { read -p " -Enter your azure secret (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_secret read -p " -Enter your azure tenant (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_tenant read -p " -Enter your azure client_id (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_client_id read -p " -Enter your azure subscription_id (https://docs.ansible.com/ansible/guide_azure.html#authenticating-with-azure) +Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials [...]: " -rs azure_subscription_id - read -p " Name the vpn server: [algo]: " -r azure_server_name From c52024d4cc404f3ebc7ed72e6c985ea8ae148ca6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 21:54:21 +0300 Subject: [PATCH 333/769] Azure. Add to the inventory #30 --- roles/cloud-azure/tasks/main.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 6a89a3b1..0a67ae82 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -77,3 +77,17 @@ - set_fact: cloud_instance_ip: "{{ ip_address }}" + +- name: Ensure the group azure exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[azure]' + +- name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[azure\]' + regexp: "^{{ cloud_instance_ip }}.*" + line: "{{ cloud_instance_ip }}" From 0aff3ebb6f098adee11eb95af945b447af08513e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 6 Mar 2017 22:04:00 +0300 Subject: [PATCH 334/769] EC2 instance_initiated_shutdown_behavior to terminate. Close #124 --- roles/cloud-ec2/tasks/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 00a87dd6..703b1d04 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -101,6 +101,7 @@ count_tag: name: "{{ aws_server_name }}" assign_public_ip: yes + instance_initiated_shutdown_behavior: terminate register: ec2 - name: Add new instance to host group From fc30f8bb10040c6cc702e621716db56d4d517b17 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 9 Mar 2017 00:41:13 +0300 Subject: [PATCH 335/769] GCE. Tags fixed #267 --- roles/cloud-gce/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 6464cba8..a91303fd 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -13,7 +13,7 @@ project_id: "{{ credentials_file_lookup.project_id }}" metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' tags: - - "environment: algo" + - "environment-algo" register: google_vm - name: Add the instance to an inventory group From 573c2f2322c60d78a3f4c901d35d63aa1590b099 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 12 Mar 2017 11:31:05 +0300 Subject: [PATCH 336/769] DO. env variables #195 --- roles/cloud-digitalocean/tasks/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 41ca4a11..2251f847 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,6 +1,6 @@ - name: Set the DigitalOcean Access Token fact set_fact: - do_token: "{{ do_access_token }}" + do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN')) }}" public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - block: @@ -8,7 +8,7 @@ digital_ocean: state: absent command: ssh - api_token: "{{ do_access_token }}" + api_token: "{{ do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys until: ssh_keys.changed != true @@ -20,7 +20,7 @@ digital_ocean: state: absent command: ssh - api_token: "{{ do_access_token }}" + api_token: "{{ do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys ignore_errors: yes @@ -35,7 +35,7 @@ state: present command: ssh ssh_pub_key: "{{ public_key }}" - api_token: "{{ do_access_token }}" + api_token: "{{ do_token }}" name: "{{ SSH_keys.comment }}" register: do_ssh_key @@ -49,7 +49,7 @@ image_id: "ubuntu-16-04-x64" ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" unique_name: yes - api_token: "{{ do_access_token }}" + api_token: "{{ do_token }}" ipv6: yes register: do @@ -60,7 +60,7 @@ ansible_ssh_user: root ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - do_access_token: "{{ do_access_token }}" + do_access_token: "{{ do_token }}" do_droplet_id: "{{ do.droplet.id }}" cloud_provider: digitalocean ipv6_support: true @@ -72,7 +72,7 @@ digital_ocean_tag: name: "Environment:Algo" resource_id: "{{ do.droplet.id }}" - api_token: "{{ do_access_token }}" + api_token: "{{ do_token }}" state: present - name: Get droplets @@ -82,7 +82,7 @@ status_code: 200 headers: Content-Type: "application/json" - Authorization: "Bearer {{ do_access_token }}" + Authorization: "Bearer {{ do_token }}" register: do_droplets - name: Ensure the group digitalocean exists in the dynamic inventory file From 906d962d4d978a44a1161db5d7023b9abc1b1ff2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 12 Mar 2017 11:32:36 +0300 Subject: [PATCH 337/769] GCE. env variables #195 --- roles/cloud-gce/tasks/main.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index a91303fd..9339113c 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,16 +1,23 @@ - set_fact: - credentials_file_lookup: "{{ lookup('file', '{{ credentials_file }}') }}" + credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH')) }}" ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" +- set_fact: + credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" + +- set_fact: + service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" + project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + - name: "Creating a new instance..." gce: instance_names: "{{ server_name }}" zone: "{{ zone }}" machine_type: f1-micro image: ubuntu-1604 - service_account_email: "{{ credentials_file_lookup.client_email }}" - credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' tags: - "environment-algo" From 045ff4bb9f4720739632aac2951fbf2798a99c8c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 14 Mar 2017 23:33:37 +0300 Subject: [PATCH 338/769] Azure security group. Fixes #264 --- roles/cloud-azure/tasks/main.yml | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 0a67ae82..dfdde2e5 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -26,6 +26,35 @@ tags: Environment: Algo +- name: Create a security group + azure_rm_securitygroup: + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + resource_group: "{{ resource_group }}" + name: AlgoSecGroup + purge_rules: yes + rules: + - name: AllowSSH + protocol: Tcp + destination_port_range: 22 + access: Allow + priority: 100 + direction: Inbound + - name: AllowIPSEC500 + protocol: Udp + destination_port_range: 500 + access: Allow + priority: 110 + direction: Inbound + - name: AllowIPSEC4500 + protocol: Udp + destination_port_range: 4500 + access: Allow + priority: 120 + direction: Inbound + - name: Create a subnet azure_rm_subnet: secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" @@ -36,6 +65,7 @@ name: algo_subnet address_prefix: "10.10.0.0/24" virtual_network: algo_net + security_group_name: AlgoSecGroup tags: Environment: Algo @@ -64,6 +94,19 @@ - set_fact: ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" + networkinterface_name: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].name }}" + +- name: Ensure the network interface includes all required parameters + azure_rm_networkinterface: + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + name: "{{ networkinterface_name }}" + resource_group: "{{ resource_group }}" + virtual_network_name: algo_net + subnet_name: algo_subnet + security_group_name: AlgoSecGroup - name: Add the instance to an inventory group add_host: From 49ba1f76b45a51bc76ef8ace8b2431fad6f804c5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 18 Mar 2017 11:07:56 +0300 Subject: [PATCH 339/769] Some improvements in the mobileconfig. Fixes #270 --- algo | 4 ++-- roles/vpn/templates/mobileconfig.j2 | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/algo b/algo index d82bad85..3364e7ed 100755 --- a/algo +++ b/algo @@ -7,13 +7,13 @@ SKIP_TAGS="_null encrypted" additional_roles () { read -p " -Do you want to enable VPN Always-On when connected to cellular networks? +Do you want to enable VPN On Demand when connected to cellular networks? [y/N]: " -r OnDemandEnabled_Cellular OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi read -p " -Do you want to enable VPN Always-On when connected to Wi-Fi? +Do you want to enable VPN On Demand when connected to Wi-Fi? [y/N]: " -r OnDemandEnabled_WIFI OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 9ee20c4f..c48bc1b9 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -62,14 +62,14 @@ IntegrityAlgorithm SHA2-256 LifeTimeInMinutes - 1440 + 20 DeadPeerDetectionRate Medium DisableMOBIKE 0 DisableRedirect - 0 + 1 EnableCertificateRevocationCheck 0 EnablePFS @@ -83,7 +83,7 @@ IntegrityAlgorithm SHA2-256 LifeTimeInMinutes - 1440 + 20
LocalIdentifier {{ item.0 }} @@ -96,7 +96,7 @@ ECDSA256 {% endif %} ServerCertificateIssuerCommonName - {{ IP_subject_alt_name }} + {{ IP_subject_alt_name }} RemoteAddress {{ IP_subject_alt_name }} RemoteIdentifier From 6facb6cb4fa428b96556c321cc54b3be2424f488 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 18 Mar 2017 12:22:07 +0300 Subject: [PATCH 340/769] FreeBSD / HardenedBSD (#262) * FreeBSD draft ifconfig fix Pre-tasks fixes fix hardcoded IP some refactoring disable system-based tags disable freebsd tags FreeBSD vpn role add defaults ssh role freebsd default fix dns_adblocking freebsd ubuntu dict fix * HardenedBSD update-users BSD * Rebuild the kernel docs changing --- .travis.yml | 2 +- docs/FreeBSD.md | 28 +++ playbooks/common.yml | 18 +- playbooks/facts/FreeBSD.yml | 10 + playbooks/freebsd.yml | 9 + playbooks/ubuntu.yml | 9 + roles/common/handlers/main.yml | 10 + roles/common/tasks/freebsd.yml | 51 ++++ roles/common/tasks/main.yml | 93 +------- roles/common/tasks/ubuntu.yml | 91 ++++++++ roles/dns_adblocking/tasks/freebsd.yml | 4 + roles/dns_adblocking/tasks/main.yml | 36 +-- roles/dns_adblocking/tasks/ubuntu.yml | 21 ++ roles/security/handlers/main.yml | 2 +- roles/ssh_tunneling/handlers/main.yml | 2 +- roles/ssh_tunneling/tasks/main.yml | 4 +- roles/vpn/tasks/client_configs.yml | 79 +++++++ roles/vpn/tasks/distribute_keys.yml | 27 +++ roles/vpn/tasks/freebsd.yml | 113 +++++++++ roles/vpn/tasks/ipec_configuration.yml | 46 ++++ roles/vpn/tasks/main.yml | 307 ++----------------------- roles/vpn/tasks/openssl.yml | 117 ++++++++++ roles/vpn/tasks/ubuntu.yml | 52 +++++ roles/vpn/templates/strongswan.conf.j2 | 10 + users.yml | 5 +- 25 files changed, 732 insertions(+), 414 deletions(-) create mode 100644 docs/FreeBSD.md create mode 100644 playbooks/facts/FreeBSD.yml create mode 100644 playbooks/freebsd.yml create mode 100644 playbooks/ubuntu.yml create mode 100644 roles/common/tasks/freebsd.yml create mode 100644 roles/common/tasks/ubuntu.yml create mode 100644 roles/dns_adblocking/tasks/freebsd.yml create mode 100644 roles/dns_adblocking/tasks/ubuntu.yml create mode 100644 roles/vpn/tasks/client_configs.yml create mode 100644 roles/vpn/tasks/distribute_keys.yml create mode 100644 roles/vpn/tasks/freebsd.yml create mode 100644 roles/vpn/tasks/ipec_configuration.yml create mode 100644 roles/vpn/tasks/openssl.yml create mode 100644 roles/vpn/tasks/ubuntu.yml diff --git a/.travis.yml b/.travis.yml index 904dbdbe..7b2a394e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,4 +48,4 @@ script: - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" after_script: - - ./tests/update-users.sh \ No newline at end of file + - ./tests/update-users.sh diff --git a/docs/FreeBSD.md b/docs/FreeBSD.md new file mode 100644 index 00000000..f1a8c838 --- /dev/null +++ b/docs/FreeBSD.md @@ -0,0 +1,28 @@ +# FreeBSD / HardenedBSD + +It is only possible to install Algo on existing systems only. We support only 11 version for now. + +## Pre-paring the system + +Ensure that the following kernel options are enabled: + +``` +# sysctl kern.conftxt | grep -iE "IPSEC|crypto" +options IPSEC +options IPSEC_NAT_T +device crypto +``` + +## Available roles + +* vpn +* ssh_tunneling +* dns_adblocking + +## Additional variables + +* rebuild_kernel - set to `true` if you want to let Algo to rebuild your kernel if needed (Takes a lot of time) + +## Installation + +`ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$server_ip Store_CAKEY=N" --skip-tags cloud` diff --git a/playbooks/common.yml b/playbooks/common.yml index c195b13d..3dce6384 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -1,10 +1,16 @@ -- name: Install prerequisites - raw: sleep 10 && sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 +--- -- name: Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - tags: - - update-alternatives +- name: Check the system + raw: uname -a + register: OS + +- name: Ubuntu pre-tasks + include: ubuntu.yml + when: '"Ubuntu" in OS.stdout' + +- name: FreeBSD pre-tasks + include: freebsd.yml + when: '"FreeBSD" in OS.stdout' - name: Ensure the algo ssh key exist on the server authorized_key: diff --git a/playbooks/facts/FreeBSD.yml b/playbooks/facts/FreeBSD.yml new file mode 100644 index 00000000..0d025fc0 --- /dev/null +++ b/playbooks/facts/FreeBSD.yml @@ -0,0 +1,10 @@ +--- + +- set_fact: + config_prefix: "/usr/local/" + root_group: wheel + ssh_service_name: sshd + apparmor_enabled: false + strongswan_additional_plugins: + - kernel-pfroute + - kernel-pfkey diff --git a/playbooks/freebsd.yml b/playbooks/freebsd.yml new file mode 100644 index 00000000..8cf0579f --- /dev/null +++ b/playbooks/freebsd.yml @@ -0,0 +1,9 @@ +--- + +- name: FreeBSD / HardenedBSD | Install prerequisites + raw: sleep 10 && env ASSUME_ALWAYS_YES=YES sudo pkg install -y python27 + +- name: FreeBSD / HardenedBSD | Configure defaults + raw: sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7 + +- include: facts/FreeBSD.yml diff --git a/playbooks/ubuntu.yml b/playbooks/ubuntu.yml new file mode 100644 index 00000000..d67cbde4 --- /dev/null +++ b/playbooks/ubuntu.yml @@ -0,0 +1,9 @@ +--- + +- name: Ubuntu | Install prerequisites + raw: sleep 10 && sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + +- name: Ubuntu | Configure defaults + raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 + tags: + - update-alternatives diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index c2296850..2272403c 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -1,8 +1,18 @@ - name: restart rsyslog service: name=rsyslog state=restarted +- name: restart ipfw + service: name=ipfw state=restarted + - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush - name: restart loopback shell: ifdown lo:100 && ifup lo:100 + +- name: restart loopback bsd + shell: > + ifconfig lo100 destroy || true && + ifconfig lo100 create && + ifconfig lo100 inet {{ local_service_ip }} netmask 255.255.255.255 && + ifconfig lo100 inet6 FCAA::1/64; echo $? diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml new file mode 100644 index 00000000..bf861084 --- /dev/null +++ b/roles/common/tasks/freebsd.yml @@ -0,0 +1,51 @@ +--- + +- set_fact: + tools: + - git + - subversion + - screen + - coreutils + - openssl + - bash + - wget + sysctl: + forwarding: + - net.inet.ip.forwarding + - net.inet6.ip6.forwarding + tags: + - always + +- name: Loopback included into the rc config + blockinfile: + dest: /etc/rc.conf + create: yes + block: | + cloned_interfaces="lo100" + ifconfig_lo100="inet {{ local_service_ip }} netmask 255.255.255.255" + ifconfig_lo100="inet6 FCAA::1/64" + notify: + - restart loopback bsd + tags: + - always + +- name: Enable the gateway features + lineinfile: dest=/etc/rc.conf regexp='^{{ item.param }}.*' line='{{ item.param }}={{ item.value }}' + with_items: + - { param: firewall_enable, value: '"YES"' } + - { param: firewall_type, value: '"open"' } + - { param: gateway_enable, value: '"YES"' } + - { param: natd_enable, value: '"YES"' } + - { param: natd_interface, value: '"{{ ansible_default_ipv4.device|default() }}"' } + - { param: natd_flags, value: '"-dynamic -m"' } + notify: + - restart ipfw + tags: + - always + +- name: FreeBSD | Activate IPFW + shell: > + kldstat -n ipfw.ko || kldload ipfw ; sysctl net.inet.ip.fw.enable=0 && + bash /etc/rc.firewall && sysctl net.inet.ip.fw.enable=1 + +- meta: flush_handlers diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 1262d3fc..d8f6ec3e 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -5,101 +5,24 @@ tags: - always -- name: Install software updates - apt: update_cache=yes upgrade=dist - tags: - - cloud +- include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- name: Check if reboot is required - shell: > - if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi - args: - executable: /bin/bash - register: reboot_required - tags: - - cloud - -- name: Reboot - shell: sleep 2 && shutdown -r now "Ansible updates triggered" - async: 1 - poll: 0 - when: reboot_required is defined and reboot_required.stdout == 'required' - ignore_errors: true - tags: - - cloud - -- name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ inventory_hostname }}" - search_regex: OpenSSH - delay: 10 - timeout: 320 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - tags: - - cloud - -- name: Disable MOTD on login and SSHD - replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" - with_items: - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } - tags: - - cloud +- include: freebsd.yml + when: ansible_distribution == 'FreeBSD' - name: Install tools - apt: name="{{ item }}" state=latest + package: name="{{ item }}" state=present with_items: - - git - - screen - - apparmor-utils - - uuid-runtime - - coreutils - - sendmail - - iptables-persistent - - cgroup-tools - - openssl - tags: - - always - -- name: Loopback for services configured - template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg - notify: - - restart loopback - tags: - - always - -- name: Loopback included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present - notify: - - restart loopback - tags: - - always - -- meta: flush_handlers + - "{{ tools }}" tags: - always - name: Enable packet forwarding for IPv4 sysctl: name="{{ item }}" value=1 with_items: - - net.ipv4.ip_forward - - net.ipv4.conf.all.forwarding + - "{{ sysctl.forwarding }}" tags: - always -- name: Enable packet forwarding for IPv6 - sysctl: name=net.ipv6.conf.all.forwarding value=1 - tags: - - always - -- name: Check apparmor support - shell: apparmor_status - ignore_errors: yes - register: apparmor_status - -- set_fact: - apparmor_enabled: true - when: '"profiles are in enforce mode" in apparmor_status.stdout' +- meta: flush_handlers diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml new file mode 100644 index 00000000..e8309939 --- /dev/null +++ b/roles/common/tasks/ubuntu.yml @@ -0,0 +1,91 @@ +--- + +- name: Install software updates + apt: update_cache=yes upgrade=dist + tags: + - cloud + +- name: Check if reboot is required + shell: > + if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi + args: + executable: /bin/bash + register: reboot_required + tags: + - cloud + +- name: Reboot + shell: sleep 2 && shutdown -r now "Ansible updates triggered" + async: 1 + poll: 0 + when: reboot_required is defined and reboot_required.stdout == 'required' + ignore_errors: true + tags: + - cloud + +- name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ inventory_hostname }}" + search_regex: OpenSSH + delay: 10 + timeout: 320 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false + tags: + - cloud + +- name: Disable MOTD on login and SSHD + replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" + with_items: + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } + tags: + - cloud + +- name: Loopback for services configured + template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg + notify: + - restart loopback + tags: + - always + +- name: Loopback included into the network config + lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present + notify: + - restart loopback + tags: + - always + +- meta: flush_handlers + tags: + - always + +- name: Check apparmor support + shell: apparmor_status + ignore_errors: yes + register: apparmor_status + +- set_fact: + apparmor_enabled: true + when: '"profiles are in enforce mode" in apparmor_status.stdout' + +- set_fact: + tools: + - git + - screen + - apparmor-utils + - uuid-runtime + - coreutils + - sendmail + - iptables-persistent + - cgroup-tools + - openssl + sysctl: + forwarding: + - net.ipv4.ip_forward + - net.ipv4.conf.all.forwarding + - net.ipv6.conf.all.forwarding + tags: + - always diff --git a/roles/dns_adblocking/tasks/freebsd.yml b/roles/dns_adblocking/tasks/freebsd.yml new file mode 100644 index 00000000..a08e2342 --- /dev/null +++ b/roles/dns_adblocking/tasks/freebsd.yml @@ -0,0 +1,4 @@ +--- + +- name: FreeBSD / HardenedBSD | Enable dnsmasq + lineinfile: dest=/etc/rc.conf regexp=^dnsmasq_enable= line='dnsmasq_enable="YES"' diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index bf589319..90a86ee3 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -2,55 +2,41 @@ setup: - name: Dnsmasq installed - apt: name=dnsmasq state=latest + package: name=dnsmasq -- name: Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 - when: apparmor_enabled is defined and apparmor_enabled == true - notify: - - restart dnsmasq +- name: Ensure that the dnsmasq user exist + user: name=dnsmasq groups=nogroup append=yes state=present - name: The dnsmasq directory created file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup -- name: Enforce the dnsmasq AppArmor policy - shell: aa-enforce usr.sbin.dnsmasq - when: apparmor_enabled is defined and apparmor_enabled == true - tags: ['apparmor'] +- include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- name: Ensure that the dnsmasq service directory exist - file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root - -- name: Setup the cgroup limitations for the ipsec daemon - template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf - notify: - - daemon-reload - - restart dnsmasq +- include: freebsd.yml + when: ansible_distribution == 'FreeBSD' - meta: flush_handlers - name: Dnsmasq configured - template: src=dnsmasq.conf.j2 dest=/etc/dnsmasq.conf + template: src=dnsmasq.conf.j2 dest="{{ config_prefix|default('/') }}etc/dnsmasq.conf" notify: - restart dnsmasq - name: Adblock script created - template: src=adblock.sh dest=/opt/adblock.sh owner=root group=root mode=0755 + template: src=adblock.sh dest=/usr/local/sbin/adblock.sh owner=root group="{{ root_group|default('root') }}" mode=0755 - name: Adblock script added to cron cron: name: Adblock hosts update minute: 10 hour: 2 - job: /opt/adblock.sh + job: /usr/local/sbin/adblock.sh user: dnsmasq - name: Update adblock hosts shell: > - /opt/adblock.sh - become: true - become_user: dnsmasq + sudo -u dnsmasq "/usr/local/sbin/adblock.sh" - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes - diff --git a/roles/dns_adblocking/tasks/ubuntu.yml b/roles/dns_adblocking/tasks/ubuntu.yml new file mode 100644 index 00000000..f0ffb915 --- /dev/null +++ b/roles/dns_adblocking/tasks/ubuntu.yml @@ -0,0 +1,21 @@ +--- + +- name: Ubuntu | Dnsmasq profile for apparmor configured + template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 + when: apparmor_enabled is defined and apparmor_enabled == true + notify: + - restart dnsmasq + +- name: Ubuntu | Enforce the dnsmasq AppArmor policy + shell: aa-enforce usr.sbin.dnsmasq + when: apparmor_enabled is defined and apparmor_enabled == true + tags: ['apparmor'] + +- name: Ubuntu | Ensure that the dnsmasq service directory exist + file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root + +- name: Ubuntu | Setup the cgroup limitations for the ipsec daemon + template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf + notify: + - daemon-reload + - restart dnsmasq diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml index e6d614b7..ab98db63 100644 --- a/roles/security/handlers/main.yml +++ b/roles/security/handlers/main.yml @@ -1,5 +1,5 @@ - name: restart ssh - service: name=ssh state=restarted + service: name="{{ ssh_service_name|default('ssh') }}" state=restarted - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/ssh_tunneling/handlers/main.yml b/roles/ssh_tunneling/handlers/main.yml index 276ebfe6..066d9600 100644 --- a/roles/ssh_tunneling/handlers/main.yml +++ b/roles/ssh_tunneling/handlers/main.yml @@ -1,2 +1,2 @@ - name: restart ssh - service: name=ssh state=restarted + service: name="{{ ssh_service_name|default('ssh') }}" state=restarted diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 2c667ac0..1cf23684 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -6,7 +6,7 @@ - name: Ensure that the sshd_config file has desired options blockinfile: dest: /etc/ssh/sshd_config - marker: '# ANSIBLE_MANAGED_ssh_tunneling_role' + marker: '# {mark} ANSIBLE MANAGED BLOCK ssh_tunneling_role' block: | Match Group algo AllowTcpForwarding local @@ -21,7 +21,7 @@ group: name=algo state=present - name: Ensure that the jail directory exist - file: path=/var/jail/ state=directory mode=0755 owner=root group=root + file: path=/var/jail/ state=directory mode=0755 owner=root group="{{ root_group|default('root') }}" - name: Ensure that the SSH users exist user: diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml new file mode 100644 index 00000000..76f5a05a --- /dev/null +++ b/roles/vpn/tasks/client_configs.yml @@ -0,0 +1,79 @@ +--- + +- name: Register p12 PayloadContent + local_action: > + shell cat private/{{ item }}.p12 | base64 + register: PayloadContent + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" + +- name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + +- name: Build the mobileconfigs + local_action: + module: template + src: mobileconfig.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + mode: 0600 + become: no + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True + +- name: Build the strongswan app android config + local_action: + module: template + src: sswan.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.sswan + mode: 0600 + become: no + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True + +- name: Build the client ipsec config file + local_action: + module: template + src: client_ipsec.conf.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + mode: 0600 + become: no + with_items: + - "{{ users }}" + +- name: Build the client ipsec secret file + local_action: + module: template + src: client_ipsec.secrets.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + mode: 0600 + become: no + with_items: + - "{{ users }}" + +- name: Build the windows client powershell script + local_action: + module: template + src: client_windows.ps1.j2 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + mode: 0600 + become: no + when: Win10_Enabled is defined and Win10_Enabled == "Y" + with_items: "{{ users }}" + +- name: Restrict permissions for the local private directories + local_action: + module: file + path: "{{ item }}" + state: directory + mode: 0700 + become: no + with_items: + - configs/{{ IP_subject_alt_name }} diff --git a/roles/vpn/tasks/distribute_keys.yml b/roles/vpn/tasks/distribute_keys.yml new file mode 100644 index 00000000..d50ecfa4 --- /dev/null +++ b/roles/vpn/tasks/distribute_keys.yml @@ -0,0 +1,27 @@ +--- + +- name: Copy the keys to the strongswan directory + copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + with_items: + - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/cacerts/ca.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ IP_subject_alt_name }}.crt" + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ IP_subject_alt_name }}.key" + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/private/{{ IP_subject_alt_name }}.key" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + notify: + - restart strongswan diff --git a/roles/vpn/tasks/freebsd.yml b/roles/vpn/tasks/freebsd.yml new file mode 100644 index 00000000..8964faa1 --- /dev/null +++ b/roles/vpn/tasks/freebsd.yml @@ -0,0 +1,113 @@ +--- + +- name: FreeBSD / HardenedBSD | Get the existing kernel parameters + command: sysctl -b kern.conftxt + register: kern_conftxt + when: rebuild_kernel is defined and rebuild_kernel == "true" + +- name: FreeBSD / HardenedBSD | Set the rebuild_needed fact + set_fact: + rebuild_needed: true + when: item not in kern_conftxt.stdout and rebuild_kernel is defined and rebuild_kernel == "true" + with_items: + - "IPSEC" + - "IPSEC_NAT_T" + - "crypto" + +- name: FreeBSD / HardenedBSD | Make the kernel config + shell: > + sysctl -b kern.conftxt > /tmp/IPSEC + when: rebuild_needed is defined and rebuild_needed == true + +- name: FreeBSD / HardenedBSD | Ensure the all options are enabled + lineinfile: + dest: /tmp/IPSEC + line: "{{ item }}" + insertbefore: BOF + with_items: + - "options IPSEC" + - "options IPSEC_NAT_T" + - "device crypto" + when: rebuild_needed is defined and rebuild_needed == true + +- name: HardenedBSD | Determine the sources + set_fact: + sources_repo: https://github.com/HardenedBSD/hardenedBSD.git + sources_version: "hardened/{{ ansible_distribution_release.split('.')[0] }}-stable/master" + when: "'Hardened' in ansible_distribution_version" + +- name: FreeBSD | Determine the sources + set_fact: + sources_repo: https://github.com/freebsd/freebsd.git + sources_version: "stable/{{ ansible_distribution_major_version }}" + when: "'Hardened' not in ansible_distribution_version" + +- name: FreeBSD / HardenedBSD | Increase the git postBuffer size + git_config: + name: http.postBuffer + scope: global + value: 1048576000 + +- block: + - name: FreeBSD / HardenedBSD | Fetching the sources... + git: + repo: "{{ sources_repo }}" + dest: /usr/krnl_src + version: "{{ sources_version }}" + accept_hostkey: true + async: 1000 + poll: 0 + register: fetching_sources + + - name: FreeBSD / HardenedBSD | Fetching the sources... + async_status: jid={{ fetching_sources.ansible_job_id }} + when: rebuild_needed is defined and rebuild_needed == true + register: result + until: result.finished + retries: 600 + delay: 30 + rescue: + - debug: var=fetching_sources + + - fail: + msg: "Something went wrong. Check the debug output above." + +- block: + - name: FreeBSD / HardenedBSD | The kernel is being built... + shell: > + mv /tmp/IPSEC /usr/krnl_src/sys/{{ ansible_architecture }}/conf && + make buildkernel KERNCONF=IPSEC && + make installkernel KERNCONF=IPSEC + args: + chdir: /usr/krnl_src + executable: /usr/local/bin/bash + when: rebuild_needed is defined and rebuild_needed == true + async: 1000 + poll: 0 + register: building_kernel + + - name: FreeBSD / HardenedBSD | The kernel is being built... + async_status: jid={{ building_kernel.ansible_job_id }} + when: rebuild_needed is defined and rebuild_needed == true + register: result + until: result.finished + retries: 600 + delay: 30 + rescue: + - debug: var=building_kernel + + - fail: + msg: "Something went wrong. Check the debug output above." + +- name: FreeBSD / HardenedBSD | Reboot + shell: > + sleep 2 && shutdown -r now + args: + executable: /usr/local/bin/bash + when: rebuild_needed is defined and rebuild_needed == true + async: 1 + poll: 0 + ignore_errors: true + +- name: FreeBSD / HardenedBSD | Enable strongswan + lineinfile: dest=/etc/rc.conf regexp=^strongswan_enable= line='strongswan_enable="YES"' diff --git a/roles/vpn/tasks/ipec_configuration.yml b/roles/vpn/tasks/ipec_configuration.yml new file mode 100644 index 00000000..a6b1530b --- /dev/null +++ b/roles/vpn/tasks/ipec_configuration.yml @@ -0,0 +1,46 @@ +--- + +- name: Setup the config files from our templates + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + with_items: + - src: strongswan.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/strongswan.conf" + owner: root + group: "{{ root_group|default('root') }}" + mode: "0644" + - src: ipsec.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/ipsec.conf" + owner: root + group: "{{ root_group|default('root') }}" + mode: "0644" + - src: ipsec.secrets.j2 + dest: "{{ config_prefix|default('/') }}etc/ipsec.secrets" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + notify: + - restart strongswan + +- name: Get loaded plugins + shell: > + find {{ config_prefix|default('/') }}etc/strongswan.d/charon/ -type f -name '*.conf' -exec basename {} \; | cut -f1 -d. + register: strongswan_plugins + +- name: Disable unneeded plugins + lineinfile: dest="{{ config_prefix|default('/') }}etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = no' state=present + notify: + - restart strongswan + when: item not in strongswan_enabled_plugins and item not in strongswan_additional_plugins + with_items: "{{ strongswan_plugins.stdout_lines }}" + +- name: Ensure that required plugins are enabled + lineinfile: dest="{{ config_prefix|default('/') }}etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = yes' state=present + notify: + - restart strongswan + when: item in strongswan_enabled_plugins or item in strongswan_additional_plugins + with_items: "{{ strongswan_plugins.stdout_lines }}" diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 567614cd..a11e2129 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -8,7 +8,7 @@ - name: Generate password for the CA key shell: > - < /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-12};echo; + openssl rand -hex 6 register: CA_password - set_fact: @@ -21,304 +21,27 @@ algo_params: "rsa:2048" when: Win10_Enabled is defined and Win10_Enabled == "Y" -- name: Install StrongSwan - apt: name=strongswan state=latest update_cache=yes install_recommends=yes - -- name: Enforcing ipsec with apparmor - shell: aa-enforce "{{ item }}" - when: apparmor_enabled is defined and apparmor_enabled == true - with_items: - - /usr/lib/ipsec/charon - - /usr/lib/ipsec/lookip - - /usr/lib/ipsec/stroke - notify: - - restart apparmor - tags: ['apparmor'] - -- name: Enable services - service: name={{ item }} enabled=yes - with_items: - - apparmor - - strongswan - - netfilter-persistent - -- name: Configure iptables so IPSec traffic can traverse the tunnel - iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE - when: (security_enabled is not defined) or - (security_enabled is defined and security_enabled != "y") - notify: - - save iptables - -- name: Configure ip6tables so IPSec traffic can traverse the tunnel - iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - when: ((security_enabled is not defined) or (security_enabled is defined and security_enabled != "y")) and - (ipv6_support is defined and ipv6_support == true) - notify: - - save iptables - - name: Ensure that the strongswan group exist group: name=strongswan state=present - name: Ensure that the strongswan user exist user: name=strongswan group=strongswan state=present -- name: Ensure that the strongswan service directory exist - file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root +- include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- name: Setup the cgroup limitations for the ipsec daemon - template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf - notify: - - daemon-reload - - restart strongswan +- include: freebsd.yml + when: ansible_distribution == 'FreeBSD' + +- name: Install StrongSwan + package: name=strongswan state=present + +- include: ipec_configuration.yml +- include: openssl.yml +- include: distribute_keys.yml +- include: client_configs.yml - meta: flush_handlers -- name: Setup the strongswan.conf file from our template - template: src=strongswan.conf.j2 dest=/etc/strongswan.conf owner=root group=root mode=0644 - notify: - - restart strongswan - -- name: Setup the ipsec.conf file from our template - template: src=ipsec.conf.j2 dest=/etc/ipsec.conf owner=root group=root mode=0644 - notify: - - restart strongswan - -- name: Setup the ipsec.secrets file - template: src=ipsec.secrets.j2 dest=/etc/ipsec.secrets owner=strongswan group=root mode=0600 - notify: - - restart strongswan - -- name: Get loaded plugins - shell: > - find /etc/strongswan.d/charon/ -type f -name '*.conf' -printf '%f\n' | cut -f1 -d. - register: strongswan_plugins - -- name: Disable unneeded plugins - lineinfile: dest="/etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = no' state=present - notify: - - restart strongswan - when: item not in strongswan_enabled_plugins - with_items: "{{ strongswan_plugins.stdout_lines }}" - -- name: Ensure that required plugins are enabled - lineinfile: dest="/etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = yes' state=present - notify: - - restart strongswan - when: item in strongswan_enabled_plugins - with_items: "{{ strongswan_plugins.stdout_lines }}" - -- name: Ensure the pki directory is not exist - local_action: - module: file - dest: configs/{{ IP_subject_alt_name }}/pki - state: absent - become: no - when: easyrsa_reinit_existent == True - -- name: Ensure the pki directories are exist - local_action: - module: file - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" - state: directory - recurse: yes - become: no - with_items: - - ecparams - - certs - - crl - - newcerts - - private - - reqs - -- name: Ensure the files are exist - local_action: - module: file - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" - state: touch - become: no - with_items: - - ".rnd" - - "private/.rnd" - - "index.txt" - - "index.txt.attr" - - "serial" - -- name: Generate the openssl server configs - local_action: - module: template - src: openssl.cnf.j2 - dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" - become: no - -- name: Build the CA pair - local_action: > - shell openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && - openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch -passout pass:"{{ easyrsa_CA_password }}" && - touch {{ IP_subject_alt_name }}_ca_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: "{{ IP_subject_alt_name }}_ca_generated" - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" - -- name: Copy the CA certificate - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" - mode: 0600 - become: no - -- name: Generate the serial number - local_action: > - shell echo 01 > serial && - touch serial_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: serial_generated - -- name: Build the server pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && - openssl ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && - touch certs/{{ IP_subject_alt_name }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ IP_subject_alt_name }}_crt_generated - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" - -- name: Build the client's pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && - openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && - touch certs/{{ item }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ item }}_crt_generated - environment: - subjectAltName: "DNS:{{ item }}" - with_items: "{{ users }}" - -- name: Build the client's p12 - local_action: > - shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - -- name: Copy the p12 certificates - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 - become: no - with_items: - - "{{ users }}" - -- name: Copy the CA cert to the strongswan directory - copy: src='configs/{{ IP_subject_alt_name }}/pki/cacert.pem' dest=/etc/ipsec.d/cacerts/ca.crt owner=strongswan group=root mode=0600 - notify: - - restart strongswan - -- name: Copy the server cert to the strongswan directory - copy: src='configs/{{ IP_subject_alt_name }}/pki/certs/{{ IP_subject_alt_name }}.crt' dest=/etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt owner=strongswan group=root mode=0600 - notify: - - restart strongswan - -- name: Copy the server key to the strongswan directory - copy: src='configs/{{ IP_subject_alt_name }}/pki/private/{{ IP_subject_alt_name }}.key' dest=/etc/ipsec.d/private/{{ IP_subject_alt_name }}.key owner=strongswan group=root mode=0600 - notify: - - restart strongswan - -- name: Register p12 PayloadContent - local_action: > - shell cat private/{{ item }}.p12 | base64 - register: PayloadContent - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - -- name: Set facts for mobileconfigs - set_fact: - proxy_enabled: false - PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - -- name: Build the mobileconfigs - local_action: - module: template - src: mobileconfig.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig - mode: 0600 - become: no - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - -- name: Build the strongswan app android config - local_action: - module: template - src: sswan.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.sswan - mode: 0600 - become: no - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - -- name: Build the client ipsec config file - local_action: - module: template - src: client_ipsec.conf.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf - mode: 0600 - become: no - with_items: - - "{{ users }}" - -- name: Build the client ipsec secret file - local_action: - module: template - src: client_ipsec.secrets.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets - mode: 0600 - become: no - with_items: - - "{{ users }}" - -- name: Build the windows client powershell script - local_action: - module: template - src: client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 - mode: 0600 - become: no - when: Win10_Enabled is defined and Win10_Enabled == "Y" - with_items: "{{ users }}" - -- name: Restrict permissions for the remote private directories - file: path="{{ item }}" state=directory mode=0700 owner=strongswan group=root - with_items: - - /etc/ipsec.d/private - -- name: Restrict permissions for the local private directories - local_action: - module: file - path: "{{ item }}" - state: directory - mode: 0700 - become: no - with_items: - - configs/{{ IP_subject_alt_name }} - -- include: iptables.yml - tags: iptables +- name: StrongSwan started + service: name=strongswan state=started diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml new file mode 100644 index 00000000..8f9d52ab --- /dev/null +++ b/roles/vpn/tasks/openssl.yml @@ -0,0 +1,117 @@ +--- + +- name: Ensure the pki directory is not exist + local_action: + module: file + dest: configs/{{ IP_subject_alt_name }}/pki + state: absent + become: no + when: easyrsa_reinit_existent == True + +- name: Ensure the pki directories are exist + local_action: + module: file + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: directory + recurse: yes + become: no + with_items: + - ecparams + - certs + - crl + - newcerts + - private + - reqs + +- name: Ensure the files are exist + local_action: + module: file + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: touch + become: no + with_items: + - ".rnd" + - "private/.rnd" + - "index.txt" + - "index.txt.attr" + - "serial" + +- name: Generate the openssl server configs + local_action: + module: template + src: openssl.cnf.j2 + dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" + become: no + + +- name: Build the CA pair + local_action: > + shell openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && + openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch -passout pass:"{{ easyrsa_CA_password }}" && + touch {{ IP_subject_alt_name }}_ca_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: "{{ IP_subject_alt_name }}_ca_generated" + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + +- name: Copy the CA certificate + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" + mode: 0600 + become: no + +- name: Generate the serial number + local_action: > + shell echo 01 > serial && + touch serial_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: serial_generated + +- name: Build the server pair + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && + openssl ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && + touch certs/{{ IP_subject_alt_name }}_crt_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ IP_subject_alt_name }}_crt_generated + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + +- name: Build the client's pair + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && + openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + touch certs/{{ item }}_crt_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ item }}_crt_generated + environment: + subjectAltName: "DNS:{{ item }}" + with_items: "{{ users }}" + +- name: Build the client's p12 + local_action: > + shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" + +- name: Copy the p12 certificates + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" + mode: 0600 + become: no + with_items: + - "{{ users }}" diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml new file mode 100644 index 00000000..d00896f3 --- /dev/null +++ b/roles/vpn/tasks/ubuntu.yml @@ -0,0 +1,52 @@ +--- + +- set_fact: + strongswan_additional_plugins: [] + +- name: Ubuntu | Install StrongSwan + apt: name=strongswan state=latest update_cache=yes install_recommends=yes + +- name: Ubuntu | Enforcing ipsec with apparmor + shell: aa-enforce "{{ item }}" + when: apparmor_enabled is defined and apparmor_enabled == true + with_items: + - /usr/lib/ipsec/charon + - /usr/lib/ipsec/lookip + - /usr/lib/ipsec/stroke + notify: + - restart apparmor + tags: ['apparmor'] + +- name: Ubuntu | Enable services + service: name={{ item }} enabled=yes + with_items: + - apparmor + - strongswan + - netfilter-persistent + +- name: Ubuntu | Configure iptables so IPSec traffic can traverse the tunnel + iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE + when: (security_enabled is not defined) or + (security_enabled is defined and security_enabled != "y") + notify: + - save iptables + +- name: Ubuntu | Configure ip6tables so IPSec traffic can traverse the tunnel + iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE + when: ((security_enabled is not defined) or + (security_enabled is defined and security_enabled != "y")) and + ipv6_support is defined and ipv6_support == "yes" + notify: + - save iptables + +- name: Ubuntu | Ensure that the strongswan service directory exist + file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root + +- name: Ubuntu | Setup the cgroup limitations for the ipsec daemon + template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf + notify: + - daemon-reload + - restart strongswan + +- include: iptables.yml + tags: iptables diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index 4eab82fd..5e66cb2e 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -11,6 +11,16 @@ charon { } user = strongswan group = strongswan + + filelog { + /var/log/charon.log { + time_format = %b %e %T + ike_name = yes + append = no + default = 1 + flush_line = yes + } + } } include strongswan.d/*.conf diff --git a/users.yml b/users.yml index 105c9be8..314858dc 100644 --- a/users.yml +++ b/users.yml @@ -36,6 +36,9 @@ - config.cfg pre_tasks: + - name: Common pre-tasks + include: playbooks/common.yml + - set_fact: IP_subject_alt_name: "{{ IP_subject }}" easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" @@ -117,7 +120,7 @@ - name: Copy the revoked certificates to the vpn server copy: src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt - dest: /etc/ipsec.d/crls/{{ item }}.crt + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" when: item not in users with_items: "{{ valid_certs.stdout_lines }}" notify: From 4de4229e82ded6ccdbb57fedeb32f36142693e76 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 18 Mar 2017 12:41:26 +0300 Subject: [PATCH 341/769] Fix hardcoded names --- playbooks/local.yml | 8 ++++---- playbooks/local_ssh.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playbooks/local.yml b/playbooks/local.yml index 76274012..08748d46 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -3,15 +3,15 @@ - name: Generate the SSH private key local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" args: - creates: configs/algo.pem + creates: "{{ SSH_keys.public }}" - name: Generate the SSH public key - local_action: shell echo `ssh-keygen -y -f configs/algo.pem` {{ SSH_keys.comment }} > {{ SSH_keys.public }} + local_action: shell echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} > {{ SSH_keys.public }} args: - creates: configs/algo.pem.pub + creates: "{{ SSH_keys.public }}" - name: Change mode for the SSH private key - local_action: file path=configs/algo.pem mode=0600 + local_action: file path={{ SSH_keys.private }} mode=0600 - name: Ensure the dynamic inventory exists blockinfile: diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml index 20b8886d..3e8d78ec 100644 --- a/playbooks/local_ssh.yml +++ b/playbooks/local_ssh.yml @@ -9,7 +9,7 @@ - name: Copy the algo ssh key to the local ssh directory local_action: module: copy - src: configs/algo.pem + src: "{{ SSH_keys.private }}" dest: ~/.ssh/algo.pem mode: '0600' From 970e5b1f449408594580bd09228b883d249cb639 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 18 Mar 2017 17:29:25 -0400 Subject: [PATCH 342/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 3c627c22..712334d8 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -4,7 +4,8 @@ 2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) 3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) -5. [I have a problem not covered here](#5-i-have-a-problem-not-covered-here) +5. +6. [I have a problem not covered here](i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -61,7 +62,17 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. -### 5. I have a problem not covered here +### 5. Bad owner or permissions on .ssh + +You tried to run Algo and it quickly exits with an error about a bad owner or permissions: + +``` +fatal: [104.236.2.94]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Bad owner or permissions on /home/user/.ssh/config\r\n", "unreachable": true} +``` + +You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home/user/.ssh` and then `chmod 600 /home/user/.ssh/config`. You may need to repeat this for other files mentioned in the error message. + +### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. From 592f43d44c8466af52f7ee435ecfeaedb9e3840c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 18 Mar 2017 17:36:45 -0400 Subject: [PATCH 343/769] convention --- README.md | 2 +- docs/{Android Setup.md => ANDROID.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{Android Setup.md => ANDROID.md} (100%) diff --git a/README.md b/README.md index 0e7439b1..9140fab3 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to ### Android Devices -You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/Android%20Setup.md) for more detailed steps. +You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/ANDROID.md) for more detailed steps. ### Windows diff --git a/docs/Android Setup.md b/docs/ANDROID.md similarity index 100% rename from docs/Android Setup.md rename to docs/ANDROID.md From 9daec9be9e32c236da6fecf14fd490876faf51fc Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 19 Mar 2017 21:45:21 +0300 Subject: [PATCH 344/769] fix ssh tasks --- playbooks/local_ssh.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml index 3e8d78ec..fc754301 100644 --- a/playbooks/local_ssh.yml +++ b/playbooks/local_ssh.yml @@ -14,7 +14,8 @@ mode: '0600' - name: Configure the local ssh config - blockinfile: + local_action: + module: blockinfile dest: "~/.ssh/config" marker: "# {mark} ALGO MANAGED BLOCK {{ cloud_instance_ip|default(server_ip) }}" insertbefore: BOF From c238a686ccd85da19c4946ba4baef96249d3c143 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 24 Mar 2017 16:44:25 -0400 Subject: [PATCH 345/769] clarify the new virtual environment instructions --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9140fab3..ad0f94dd 100644 --- a/README.md +++ b/README.md @@ -41,20 +41,14 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua Windows: See the [Windows documentation](docs/WINDOWS.md) -4. Configure and initialize a python virtual environment to manage Algo's python dependencies. Again from the directory where you have downloaded Algo, run: +4. Install Algo's remaining dependencies for your operating system. `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` - Important: the virtual environment needs to be active whenever you are running Algo commands. This means that if you, for example, need to add or remove users, you must run - - `source env/bin/activate` - - first. - 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). -That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. +That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. You can now setup a client to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](https://github.com/trailofbits/algo#configure-the-vpn-clients) below. ``` "\"#----------------------------------------------------------------------#\"", @@ -68,7 +62,9 @@ That's it! You will get the message below when the server deployment process com "\"#----------------------------------------------------------------------#\"", ``` -Note: Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. +Note: If you want to run Algo at any point in the future, you must first "reactivate" the dependencies for it. To reactivate them, open your terminal, use `cd` to navigate to the directory with Algo, then run `source env/bin/activate`. For example, they should be activated before you run the [update-users script](https://github.com/trailofbits/algo#adding-or-removing-users). + +Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. ## Configure the VPN Clients @@ -121,7 +117,8 @@ Use the example command below to start an SSH tunnel by replacing `user` and `ip Algo's own scripts can easily add and remove users from the VPN server. 1. Update the `users` list in your `config.cfg` -2. Run the command: `./algo update-users` +2. Open a terminal, `cd` to the algo directory, and activate the virtual environment with `source env/bin/activate` +3. Run the command: `./algo update-users` The Algo VPN server now contains only the users listed in the `config.cfg` file. From 27e0fd073b3e56b64854a24b52a09840ea0c5763 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 24 Mar 2017 16:45:11 -0400 Subject: [PATCH 346/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad0f94dd..75d17e4d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua Windows: See the [Windows documentation](docs/WINDOWS.md) -4. Install Algo's remaining dependencies for your operating system. +4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step, run: `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` From 1a3341c449a6c480411ef56e36bc4289a8a77203 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 25 Mar 2017 19:05:00 -0400 Subject: [PATCH 347/769] add python2 vs 3 issue --- docs/TROUBLESHOOTING.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 712334d8..e04d787a 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -4,12 +4,13 @@ 2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) 3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) -5. -6. [I have a problem not covered here](i-have-a-problem-not-covered-here) +5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) +6. [Error: "TypeError: must be str, not bytes"](#6-error-typerror-must-be-str-not-bytes) +7. [I have a problem not covered here](i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" -On macOS, did you try to install the dependencies with pip and encounter the following error? +On macOS, you tried to install the dependencies with pip and encountered the following error: ``` Downloading cffi-1.9.1.tar.gz (407kB): 407kB downloaded @@ -33,7 +34,7 @@ The Xcode compiler is installed but requires you to accept its license agreement ### 2. Error: "fatal error: 'openssl/opensslv.h' file not found" -On macOS, did you try to install pycrypto and encounter the following error? +On macOS, you tried to install pycrypto and encountered the following error: ``` build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found @@ -72,7 +73,18 @@ fatal: [104.236.2.94]: UNREACHABLE! => {"changed": false, "msg": "Failed to conn You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home/user/.ssh` and then `chmod 600 /home/user/.ssh/config`. You may need to repeat this for other files mentioned in the error message. +### 6. Error: "TypeError: must be str, not bytes" + +You tried to install Algo and you see many repeated errors referencing `TypeError`, such as `TypeError: '>=' not supported between instances of 'TypeError' and 'int'` and `TypeError: must be str, not bytes`. For example: + +``` +TASK [Wait until SSH becomes ready...] ***************************************** +An exception occurred during task execution. To see the full traceback, use -vvv. The error was: TypeError: must be str, not bytes +fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, "module_stderr": "Traceback (most recent call last):\n File \"/var/folders/x_/nvr61v455qq98vp22k5r5vm40000gn/T/ansible_6sdjysth/ansible_module_wait_for.py\", line 538, in \n main()\n File \"/var/folders/x_/nvr61v455qq98vp22k5r5vm40000gn/T/ansible_6sdjysth/ansible_module_wait_for.py\", line 483, in main\n data += response\nTypeError: must be str, not bytes\n", "module_stdout": "", "msg": "MODULE FAILURE"} +``` + +You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.com/ansible/ansible) which has issues with Python3, although this situation is improving over time. Try running Algo with Python2 to fix this issue. Open your terminal and `cd` to the directory with Algo, then run: ``virtualenv -p `which python2.7` env && source env/bin/activate && pip install -r requirements.txt`` + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. - From 40d03e257a4c01b21da6806d4fc05bd8de557643 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 25 Mar 2017 19:05:23 -0400 Subject: [PATCH 348/769] typo --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e04d787a..d1bf7504 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -6,7 +6,7 @@ 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) 6. [Error: "TypeError: must be str, not bytes"](#6-error-typerror-must-be-str-not-bytes) -7. [I have a problem not covered here](i-have-a-problem-not-covered-here) +7. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From 9fe8965ccdcfe5b1b51e95734d8bb85167bc0fcd Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 25 Mar 2017 19:06:11 -0400 Subject: [PATCH 349/769] another typo --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index d1bf7504..57f4dbaf 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -5,7 +5,7 @@ 3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) -6. [Error: "TypeError: must be str, not bytes"](#6-error-typerror-must-be-str-not-bytes) +6. [Error: "TypeError: must be str, not bytes"](#6-error-typeerror-must-be-str-not-bytes) 7. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From a558b5b549e714b248b0260c915d83d02628c5ae Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 26 Mar 2017 23:37:02 -0400 Subject: [PATCH 350/769] fix mac install instructions --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75d17e4d..7364514b 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,14 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step, run: - `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` + `sudo pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). -That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. You can now setup a client to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](https://github.com/trailofbits/algo#configure-the-vpn-clients) below. +That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. + +You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](https://github.com/trailofbits/algo#configure-the-vpn-clients) below. ``` "\"#----------------------------------------------------------------------#\"", @@ -62,7 +64,7 @@ That's it! You will get the message below when the server deployment process com "\"#----------------------------------------------------------------------#\"", ``` -Note: If you want to run Algo at any point in the future, you must first "reactivate" the dependencies for it. To reactivate them, open your terminal, use `cd` to navigate to the directory with Algo, then run `source env/bin/activate`. For example, they should be activated before you run the [update-users script](https://github.com/trailofbits/algo#adding-or-removing-users). +Note: If you want to run Algo again at any point in the future, you must first "reactivate" the dependencies for it. To reactivate them, open your terminal, use `cd` to navigate to the directory with Algo, then run `source env/bin/activate`. Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. From 655a917dd277e67a83744600f1513b187d846807 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 27 Mar 2017 00:04:46 -0400 Subject: [PATCH 351/769] iptables filter table fix (#285) --- roles/vpn/tasks/ubuntu.yml | 15 --------------- roles/vpn/templates/rules.v4.j2 | 1 + roles/vpn/templates/rules.v6.j2 | 1 + 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index d00896f3..dbd459fe 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -24,21 +24,6 @@ - strongswan - netfilter-persistent -- name: Ubuntu | Configure iptables so IPSec traffic can traverse the tunnel - iptables: table=nat chain=POSTROUTING source="{{ vpn_network }}" jump=MASQUERADE - when: (security_enabled is not defined) or - (security_enabled is defined and security_enabled != "y") - notify: - - save iptables - -- name: Ubuntu | Configure ip6tables so IPSec traffic can traverse the tunnel - iptables: ip_version=ipv6 table=nat chain=POSTROUTING source="{{ vpn_network_ipv6 }}" jump=MASQUERADE - when: ((security_enabled is not defined) or - (security_enabled is defined and security_enabled != "y")) and - ipv6_support is defined and ipv6_support == "yes" - notify: - - save iptables - - name: Ubuntu | Ensure that the strongswan service directory exist file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 77fa27b3..5ced4eeb 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -41,3 +41,4 @@ COMMIT -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT COMMIT + diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index fffd3668..0eda48fc 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -55,3 +55,4 @@ COMMIT -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " -A ICMPV6-CHECK-LOG -j DROP COMMIT + From 3aa5383b74d3cd1c5e72ae67dae296bae22d1c1d Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 27 Mar 2017 01:24:42 -0400 Subject: [PATCH 352/769] add FAQ about new regions --- docs/TROUBLESHOOTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 57f4dbaf..885e4b99 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -6,7 +6,8 @@ 4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) 6. [Error: "TypeError: must be str, not bytes"](#6-error-typeerror-must-be-str-not-bytes) -7. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +7. [The region you want is not available](#7-the-region-you-want-is-not-available) +8. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -85,6 +86,10 @@ fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, " You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.com/ansible/ansible) which has issues with Python3, although this situation is improving over time. Try running Algo with Python2 to fix this issue. Open your terminal and `cd` to the directory with Algo, then run: ``virtualenv -p `which python2.7` env && source env/bin/activate && pip install -r requirements.txt`` +### 7. The region you want is not available + +You want to install Algo to a specific region in a cloud provider, but that region is not available in the list give to you by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. From 8fbc1348e0937675dc57497ad576b73836e74047 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 27 Mar 2017 01:33:46 -0400 Subject: [PATCH 353/769] typo --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 885e4b99..8decfe7d 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -88,7 +88,7 @@ You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.c ### 7. The region you want is not available -You want to install Algo to a specific region in a cloud provider, but that region is not available in the list give to you by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. +You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. ### I have a problem not covered here From 6f48ff4d9d66fa28ba029baf3853d6cd31c9ac9c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 27 Mar 2017 17:40:34 -0400 Subject: [PATCH 354/769] clarity --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7364514b..f859d875 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPS The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. 1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) -3. Install Algo's core dependencies for your operating system. To do this, open a terminal and `cd` into the directory where you downloaded Algo, then: +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convienent location. +3. Install Algo's core dependencies for your operating system. To do this, open the Terminal app and `cd` into the directory where you downloaded Algo, then run: macOS: `sudo easy_install pip` @@ -41,9 +41,9 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua Windows: See the [Windows documentation](docs/WINDOWS.md) -4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step, run: +4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step and run the command below. If you're on macOS, this may prompt you to install `cc` which you should accept. - `sudo pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` + `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. 6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). From 770e1bbe7fb2461d3301b5db8d56921687c61ea3 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 27 Mar 2017 18:15:12 -0400 Subject: [PATCH 355/769] Update WINDOWS.md --- docs/WINDOWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index 3a53f7e2..fcd5d977 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -27,6 +27,6 @@ Install additional packages: Clone the Algo repository: -`https://github.com/trailofbits/algo && cd algo` +`git clone https://github.com/trailofbits/algo && cd algo` Now, you can go through the [README](https://github.com/trailofbits/algo#deploy-the-algo-server) (start from the 4th step) and deploy your Algo server! From 4dff2dcf95ce42d98c947dbed66f45920314ae3e Mon Sep 17 00:00:00 2001 From: Will Biddy Date: Tue, 28 Mar 2017 21:05:34 -0400 Subject: [PATCH 356/769] Fixing spelling. (#297) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f859d875..b102e335 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPS The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. 1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convienent location. +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convenient location. 3. Install Algo's core dependencies for your operating system. To do this, open the Terminal app and `cd` into the directory where you downloaded Algo, then run: macOS: `sudo easy_install pip` From 3b3fb601ef812f971fe94187669c6cda7143e544 Mon Sep 17 00:00:00 2001 From: James Hale Date: Tue, 28 Mar 2017 21:18:33 -0400 Subject: [PATCH 357/769] Fix name tag key (#282) --- roles/cloud-ec2/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 703b1d04..be0b0d4e 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -95,11 +95,11 @@ wait: true region: "{{ region }}" instance_tags: - name: "{{ aws_server_name }}" + Name: "{{ aws_server_name }}" Environment: Algo exact_count: 1 count_tag: - name: "{{ aws_server_name }}" + Name: "{{ aws_server_name }}" assign_public_ip: yes instance_initiated_shutdown_behavior: terminate register: ec2 From c4f11884346d52696e685f3eb4df9da55f2d1fe8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 28 Mar 2017 22:18:44 -0400 Subject: [PATCH 358/769] add donate via Flattr and PayPal --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b102e335..de79a84e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) +[![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) +[![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From 2b295e501c4d74b11ab0988d9b07fefe483412b3 Mon Sep 17 00:00:00 2001 From: Mike Lee Williams Date: Wed, 29 Mar 2017 00:06:06 -0400 Subject: [PATCH 359/769] Clarify python instructions (#302) - Be explicit about python2 requirement - Remove `sudo` requirement on macOS - Use `python -m pip` and `python -m virtualenv` to ensure expected python interpreter is running pip/virtualenv - Tidy formatting --- README.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index de79a84e..5db052fb 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,39 @@ Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPS The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. Setup an account on a cloud hosting provider. Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convenient location. -3. Install Algo's core dependencies for your operating system. To do this, open the Terminal app and `cd` into the directory where you downloaded Algo, then run: +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). - macOS: `sudo easy_install pip` +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convenient location on your local machine. - Linux (deb-based): `sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv -y` +3. Install Algo's core dependencies. Open the Terminal. The `python` interpreter you use to deploy Algo must be python2. If you don't know what this means, you're probably fine. `cd` into the directory where you downloaded Algo, then run: + + - macOS: + ```bash + $ python -m ensurepip --user + $ python -m pip install --user --upgrade virtualenv + ``` + - Linux (deb-based): + ```bash + $ sudo apt-get update && sudo apt-get install \ + build-essential \ + libssl-dev \ + libffi-dev \ + python-dev \ + python-pip \ + python-setuptools \ + python-virtualenv -y + ``` + - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) + - Windows: See the [Windows documentation](docs/WINDOWS.md) - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) - - Windows: See the [Windows documentation](docs/WINDOWS.md) - -4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step and run the command below. If you're on macOS, this may prompt you to install `cc` which you should accept. - - `pip install virtualenv && virtualenv env && source env/bin/activate && pip install -r requirements.txt` +4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. + ```bash + $ python -m virtualenv env && source env/bin/activate && python -m pip install -r requirements.txt + ``` + On macOS, you may be prompted to install `cc` which you should accept. 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. + 6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. From c656a61069e16a25c9f94f88e9a29eeebbf0ecf7 Mon Sep 17 00:00:00 2001 From: Marcos Ojeda Date: Tue, 28 Mar 2017 21:26:05 -0700 Subject: [PATCH 360/769] add 'back on patreon' shields.io button (#303) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5db052fb..ff4f732e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) [![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) +[![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. From c081913566698c67c1e8ab39d0b2c489375d40f2 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 30 Mar 2017 15:52:01 -0400 Subject: [PATCH 361/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 8decfe7d..53213f98 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -7,7 +7,8 @@ 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) 6. [Error: "TypeError: must be str, not bytes"](#6-error-typeerror-must-be-str-not-bytes) 7. [The region you want is not available](#7-the-region-you-want-is-not-available) -8. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +8. [Error: "ansible-playbook: command not found"](#8-ansible-playbook-command-not-found) +9. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -90,6 +91,12 @@ You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.c You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. +### 8. Error: "ansible-playbook: command not found" + +You tried to install Algo and you get an error during the install that reads "ansible-playbook: command not found." + +You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. From ea40249adfe08b2d948d59c1ca50eadc0e45bbaa Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 30 Mar 2017 15:52:34 -0400 Subject: [PATCH 362/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 53213f98..09d191ce 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -93,7 +93,7 @@ You want to install Algo to a specific region in a cloud provider, but that regi ### 8. Error: "ansible-playbook: command not found" -You tried to install Algo and you get an error during the install that reads "ansible-playbook: command not found." +You tried to install Algo and you see an error that reads "ansible-playbook: command not found." You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. From e729119b24602deab88cb7c17fd8899ebf761848 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 30 Mar 2017 15:53:30 -0400 Subject: [PATCH 363/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 09d191ce..87fb7043 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -7,7 +7,7 @@ 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) 6. [Error: "TypeError: must be str, not bytes"](#6-error-typeerror-must-be-str-not-bytes) 7. [The region you want is not available](#7-the-region-you-want-is-not-available) -8. [Error: "ansible-playbook: command not found"](#8-ansible-playbook-command-not-found) +8. [Error: "ansible-playbook: command not found"](#8-error-ansible-playbook-command-not-found) 9. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From bb55985ab4664d1a8c21e5771008344510930f9b Mon Sep 17 00:00:00 2001 From: "jeremy avnet / @brainsik" Date: Fri, 31 Mar 2017 10:25:22 -0700 Subject: [PATCH 364/769] Add GCE Western US zones (#319) --- algo | 56 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/algo b/algo index 3364e7ed..39a75e80 100755 --- a/algo +++ b/algo @@ -253,37 +253,41 @@ Name the vpn server: read -p " What zone should the server be located in? - 1. Central US (Iowa A) - 2. Central US (Iowa B) - 3. Central US (Iowa C) - 4. Central US (Iowa F) - 5. Eastern US (South Carolina B) - 6. Eastern US (South Carolina C) - 7. Eastern US (South Carolina D) - 8. Western Europe (Belgium B) - 9. Western Europe (Belgium C) - 10. Western Europe (Belgium D) - 11. East Asia (Taiwan A) - 12. East Asia (Taiwan B) - 13. East Asia (Taiwan C) + 1. Western US (Oregon A) + 2. Western US (Oregon B) + 3. Central US (Iowa A) + 4. Central US (Iowa B) + 5. Central US (Iowa C) + 6. Central US (Iowa F) + 7. Eastern US (South Carolina B) + 8. Eastern US (South Carolina C) + 9. Eastern US (South Carolina D) + 10. Western Europe (Belgium B) + 11. Western Europe (Belgium C) + 12. Western Europe (Belgium D) + 13. East Asia (Taiwan A) + 14. East Asia (Taiwan B) + 15. East Asia (Taiwan C) Please choose the number of your zone. Press enter for default (#8) zone. [8]: " -r region region=${region:-8} case "$region" in - 1) zone="us-central1-a" ;; - 2) zone="us-central1-b" ;; - 3) zone="us-central1-c" ;; - 4) zone="us-central1-f" ;; - 5) zone="us-east1-b" ;; - 6) zone="us-east1-c" ;; - 7) zone="us-east1-d" ;; - 8) zone="europe-west1-b" ;; - 9) zone="europe-west1-c" ;; - 10) zone="europe-west1-d" ;; - 11) zone="asia-east1-a" ;; - 12) zone="asia-east1-b" ;; - 13) zone="asia-east1-c" ;; + 1) zone="us-west1-a" ;; + 2) zone="us-west1-b" ;; + 3) zone="us-central1-a" ;; + 4) zone="us-central1-b" ;; + 5) zone="us-central1-c" ;; + 6) zone="us-central1-f" ;; + 7) zone="us-east1-b" ;; + 8) zone="us-east1-c" ;; + 9) zone="us-east1-d" ;; + 10) zone="europe-west1-b" ;; + 11) zone="europe-west1-c" ;; + 12) zone="europe-west1-d" ;; + 13) zone="asia-east1-a" ;; + 14) zone="asia-east1-b" ;; + 15) zone="asia-east1-c" ;; esac ROLES="gce vpn cloud" From 9b76282a37055b7dfe0c5878ae36785edbbac0e1 Mon Sep 17 00:00:00 2001 From: Casey Lang Date: Fri, 31 Mar 2017 12:25:39 -0500 Subject: [PATCH 365/769] Check for creation of private key during its generation (#322) This task was previously checking for the public key even though it is in place to generate the private key. A simple switch to the `creates` arg resolves the issue. --- playbooks/local.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/local.yml b/playbooks/local.yml index 08748d46..bea14708 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -3,7 +3,7 @@ - name: Generate the SSH private key local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" args: - creates: "{{ SSH_keys.public }}" + creates: "{{ SSH_keys.private }}" - name: Generate the SSH public key local_action: shell echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} > {{ SSH_keys.public }} From 09e5d87c7b12eb5b3fe23bcb5d1e0c1237b5d8c9 Mon Sep 17 00:00:00 2001 From: brad2014 Date: Fri, 31 Mar 2017 21:19:10 -0700 Subject: [PATCH 366/769] Minor name and documentation edits (#327) --- CONTRIBUTING.md | 2 +- README.md | 26 ++++++++++++------------- algo | 10 +++++----- config.cfg | 3 ++- deploy.yml | 2 +- docs/ADVANCED.md | 4 ++-- docs/ANDROID.md | 8 ++++---- docs/FAQ.md | 4 ++-- docs/ROLES.md | 4 ++-- docs/TROUBLESHOOTING.md | 2 +- roles/client/tasks/main.yml | 2 +- roles/security/templates/sshd_config.j2 | 2 +- roles/vpn/tasks/main.yml | 4 ++-- roles/vpn/tasks/ubuntu.yml | 2 +- 14 files changed, 38 insertions(+), 37 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b37f56f..41b0be90 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ * Check that your issue is not already described in the [FAQ](docs/FAQ.md) or [troubleshooting](docs/TROUBLESHOOTING.md) docs * Did you remember to install the dependencies for your operating system prior to installing Algo? -* Client supported is limited to only modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. +* Client support is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. * If you need to file an issue, fill out any relevant fields in the Issue Template ### Pull Requests diff --git a/README.md b/README.md index ff4f732e..347ac577 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) [![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) -Algo VPN is a set of Ansible scripts that simplifies the setup of a personal IPSEC VPN. It contains the most secure defaults available, works with common cloud providers, and does not require client software on most devices. +Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. ## Features -* Supports only IKEv2 w/ a single cipher suite: AES-GCM, HMAC-SHA2, and P-256 DH -* Generates Apple Profiles to auto-configure iOS and macOS devices -* Provides helper scripts to add and remove users +* Supports only IKEv2, with a single cipher suite: AES-GCM, HMAC-SHA2, and P-256 DH +* Generates Apple profiles to auto-configure iOS and macOS devices +* Includes helper scripts to add and remove users * Blocks ads with a local DNS resolver and HTTP proxy (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan @@ -34,16 +34,16 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and decompress it in a convenient location on your local machine. +2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and unzip it in a convenient location on your local machine. + +3. Install Algo's core dependencies. Open the Terminal. The `python` interpreter you use to deploy Algo must be python2. If you don't know what this means, you're probably fine. `cd` into the `algo-master` directory where you unzipped Algo, then run: -3. Install Algo's core dependencies. Open the Terminal. The `python` interpreter you use to deploy Algo must be python2. If you don't know what this means, you're probably fine. `cd` into the directory where you downloaded Algo, then run: - - macOS: ```bash $ python -m ensurepip --user $ python -m pip install --user --upgrade virtualenv ``` - - Linux (deb-based): + - Linux (deb-based): ```bash $ sudo apt-get update && sudo apt-get install \ build-essential \ @@ -59,7 +59,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. ```bash - $ python -m virtualenv env && source env/bin/activate && python -m pip install -r requirements.txt + $ python -m virtualenv env && source env/bin/activate && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc` which you should accept. @@ -89,7 +89,7 @@ Advanced users who want to install Algo on top of a server they already own or w ## Configure the VPN Clients -Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are prefixed with the IP address of your new Algo VPN server. +Distribute the configuration files to your users, so they can connect to the VPN. Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are saved under a subdirectory named with the IP address of your new Algo VPN server. ### Apple Devices @@ -97,7 +97,7 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to ### Android Devices -You need to install the [StrongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/ANDROID.md) for more detailed steps. +You need to install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/ANDROID.md) for more detailed steps. ### Windows @@ -117,8 +117,8 @@ Install strongSwan, then copy the included user_ipsec.conf, user_ipsec.secrets, Depending on the platform, you may need one or multiple of the following files. * ca.crt: CA Certificate -* user_ipsec.conf: StrongSwan client configuration -* user_ipsec.secrets: StrongSwan client configuration +* user_ipsec.conf: strongSwan client configuration +* user_ipsec.secrets: strongSwan client configuration * user.crt: User Certificate * user.key: User Private Key * user.mobileconfig: Apple Profile diff --git a/algo b/algo index 39a75e80..ecf33a18 100755 --- a/algo +++ b/algo @@ -7,27 +7,27 @@ SKIP_TAGS="_null encrypted" additional_roles () { read -p " -Do you want to enable VPN On Demand when connected to cellular networks? +Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to cellular networks? [y/N]: " -r OnDemandEnabled_Cellular OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi read -p " -Do you want to enable VPN On Demand when connected to Wi-Fi? +Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to Wi-Fi? [y/N]: " -r OnDemandEnabled_WIFI OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then read -p " -Do you want to exclude trusted Wi-Fi networks from using the VPN? (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) +List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) : " -r OnDemandEnabled_WIFI_EXCLUDE OnDemandEnabled_WIFI_EXCLUDE=${OnDemandEnabled_WIFI_EXCLUDE:-_null} EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=\"$OnDemandEnabled_WIFI_EXCLUDE\"" fi read -p " -Do you want to install a local DNS resolver to block ads while surfing? +Do you want to install a DNS resolver on this VPN server, to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi @@ -51,7 +51,7 @@ Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi read -p " -Do you want to store the CA key? (required for update-users script, but less secure) +Do you want to retain the CA key? (required to add users in the future, but less secure) [y/N]: " -r Store_CAKEY Store_CAKEY=${Store_CAKEY:-N} if [[ "$Store_CAKEY" =~ ^(n|N)$ ]]; then EXTRA_VARS+=" Store_CAKEY=N"; fi diff --git a/config.cfg b/config.cfg index a8c86e86..d4e9a3d3 100644 --- a/config.cfg +++ b/config.cfg @@ -1,6 +1,7 @@ --- -# Add as many users as you want for your VPN server here +# Add as many users as you want for your VPN server here. +# Access credentials will be generated for each one. users: - dan - jack diff --git a/deploy.yml b/deploy.yml index 4f967b7c..c1adc4b8 100644 --- a/deploy.yml +++ b/deploy.yml @@ -23,7 +23,7 @@ - { role: local, tags: ['local'] } post_tasks: - - name: Local pre-tasks + - name: Local post-tasks include: playbooks/post.yml become: false tags: [ 'cloud' ] diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index ab1bc1a4..6b58a10d 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -1,6 +1,6 @@ # Advanced Usage -Make sure you have installed all the dependencies necessary for your operating system as described in the README. +Make sure you have installed all the dependencies necessary for your operating system as described in the [README](../README.md). ## Local Deployment @@ -38,7 +38,7 @@ Required tags: - cloud Cloud roles: - + - role: cloud-digitalocean, tags: digitalocean - role: cloud-ec2, tags: ec2 - role: cloud-gce, tags: gce diff --git a/docs/ANDROID.md b/docs/ANDROID.md index ba42bdbb..1f6faba0 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -1,9 +1,9 @@ -**NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [StrongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. +**NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [strongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. | Instruction | Screenshot(s) | | ----------- | ---------- | | 1. Copy your `{username}.p12` certificate to your phone's internal storage. | | -| 2. [Install the StrongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android) (Android 4+) | | +| 2. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android) (Android 4+) | | | 3. Open the app and tap "ADD VPN PROFILE" in the top right. | [![step3-thumb]][step3-screen] | | 4. Enter the IP address or hostname of your Algo server and set the "VPN Type" to "IKEv2 Certificate". | [![step4-thumb]][step4-screen] | | 5. Tap "Select user certificate". You will be shown a prompt, tap "INSTALL". | [![step5-thumb]][step5-screen] | @@ -14,8 +14,8 @@ | 10. You will be returned to the main menu, and your newly-configured VPN profile should be listed. Tap the profile to connect. | [![step10-thumb]][step10-screen] | ## Troubleshooting -### Tapping the VPN profile in StrongSwan has no effect. -Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before attempting to enable a StrongSwan VPN. If any other VPN is active, StrongSwan may silently fail to initialize a VPN connection. On Android 7, your can manage your VPNs by going to: Settings > Tap "More" under "Wireless & networks" > VPN > tap the gear icon next to any non-strongSwan VPNs listed and ensure they are disabled. +### Tapping the VPN profile in strongSwan has no effect. +Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before attempting to enable a strongSwan VPN. If any other VPN is active, strongSwan may silently fail to initialize a VPN connection. On Android 7, your can manage your VPNs by going to: Settings > Tap "More" under "Wireless & networks" > VPN > tap the gear icon next to any non-strongSwan VPNs listed and ensure they are disabled. [step3-thumb]: https://i.imgur.com/LPwIGJE.png diff --git a/docs/FAQ.md b/docs/FAQ.md index 10fb6f35..e18f8523 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -18,11 +18,11 @@ The goal of this project is not to provide anonymity, but to ensure confidential ### 3. Why aren't you using Racoon, LibreSwan, or OpenSwan? -Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for StrongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. StrongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. +Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for strongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. strongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. ### 4. Why aren't you using a memory-safe or verified IKE daemon? -I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to StrongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. +I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to strongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. ### 5. Why aren't you using OpenVPN? diff --git a/docs/ROLES.md b/docs/ROLES.md index 1f438c3d..f43e6f8d 100644 --- a/docs/ROLES.md +++ b/docs/ROLES.md @@ -4,9 +4,9 @@ * **Common** * Installs several required packages and software updates, then reboots if necessary - * Configures network interfaces and enables packet forwarding on them + * Configures network interfaces, and enables packet forwarding on them * **VPN** - * Installs [StrongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges + * Installs [strongSwan](https://www.strongswan.org/), enables AppArmor, limits CPU and memory access, and drops user privileges * Builds a Certificate Authority (CA) with [easy-rsa-ipsec](https://github.com/ValdikSS/easy-rsa-ipsec) and creates one client certificate per user * Bundles the appropriate certificates into Apple mobileconfig profiles for each user * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 87fb7043..326afc58 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -99,4 +99,4 @@ You did not finish step 4 in the installation instructions, "[Install Algo's rem ### I have a problem not covered here -If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel or [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. +If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel. You may also [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index c5b69971..ae340d34 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -22,7 +22,7 @@ with_items: - "{{ prerequisites }}" -- name: Install StrongSwan +- name: Install strongSwan package: name=strongswan state=present - name: Setup the ipsec config diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index c014eb46..ebc93eeb 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -9,7 +9,7 @@ SyslogFacility AUTH LogLevel VERBOSE # Use kernel sandbox mechanisms where possible -# Systrace on OpenBSD, Seccomp on Linux, seatbelt on MacOSX/Darwin, rlimit elsewhere. +# Systrace on OpenBSD, Seccomp on Linux, seatbelt on macOS X (Darwin), rlimit elsewhere. UsePrivilegeSeparation sandbox # Handy for keeping network connections alive diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index a11e2129..5ec7f3db 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -33,7 +33,7 @@ - include: freebsd.yml when: ansible_distribution == 'FreeBSD' -- name: Install StrongSwan +- name: Install strongSwan package: name=strongswan state=present - include: ipec_configuration.yml @@ -43,5 +43,5 @@ - meta: flush_handlers -- name: StrongSwan started +- name: strongSwan started service: name=strongswan state=started diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index dbd459fe..4856a97c 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -3,7 +3,7 @@ - set_fact: strongswan_additional_plugins: [] -- name: Ubuntu | Install StrongSwan +- name: Ubuntu | Install strongSwan apt: name=strongswan state=latest update_cache=yes install_recommends=yes - name: Ubuntu | Enforcing ipsec with apparmor From 84a3b5f675ce3a202300e5352e8dbbaf268f5fc7 Mon Sep 17 00:00:00 2001 From: Josh Watson Date: Sat, 1 Apr 2017 00:20:08 -0400 Subject: [PATCH 367/769] Change EC2 VPC CIDR blocks to non-routable addresses. (#330) The previous address ranges were actually routable addresses, which caused some concern for some people because it looked suspicious in tracert. The new CIDR blocks are non-routable addresses, which resolves this concern. --- roles/cloud-ec2/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 173d9696..8ef29ce5 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -1,5 +1,5 @@ --- ec2_vpc_nets: - cidr_block: 172.251.0.0/23 - subnet_cidr: 172.251.1.0/24 + cidr_block: 192.168.0.0/23 + subnet_cidr: 192.168.1.0/24 From 946314ee26ee44e53e884bf0bc64c874ac8bbc09 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 13:49:27 -0400 Subject: [PATCH 368/769] add referral link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 347ac577..2bd6c8ac 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://www.digitalocean.com/) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). 2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and unzip it in a convenient location on your local machine. From eeae3ad34eb9a59151537628572a15e4cfca0a0c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 14:11:01 -0400 Subject: [PATCH 369/769] add info about reconfiguring the apple profile --- docs/TROUBLESHOOTING.md | 61 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 326afc58..e1c86fa3 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -2,13 +2,14 @@ 1. [Error: "You have not agreed to the Xcode license agreements"](#1-error-you-have-not-agreed-to-the-xcode-license-agreements) 2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) -3. [Little Snitch is broken when connected to the VPN](#3-little-snitch-is-broken-when-connected-to-the-vpn) -4. [Various websites appear to be offline through the VPN](#4-various-websites-appear-to-be-offline-through-the-vpn) +3. [Error: "TypeError: must be str, not bytes"](#3-error-typeerror-must-be-str-not-bytes) +4. [Error: "ansible-playbook: command not found"](#4-error-ansible-playbook-command-not-found) 5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) -6. [Error: "TypeError: must be str, not bytes"](#6-error-typeerror-must-be-str-not-bytes) -7. [The region you want is not available](#7-the-region-you-want-is-not-available) -8. [Error: "ansible-playbook: command not found"](#8-error-ansible-playbook-command-not-found) -9. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +6. [Little Snitch is broken when connected to the VPN](#6-little-snitch-is-broken-when-connected-to-the-vpn) +7. [Various websites appear to be offline through the VPN](#7-various-websites-appear-to-be-offline-through-the-vpn) +8. [The region you want is not available](#8-the-region-you-want-is-not-available) +9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-Wifi-networks-on-my-Apple-device) +10. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -57,24 +58,6 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. -### 3. Little Snitch is broken when connected to the VPN - -Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). - -### 4. Various websites appear to be offline through the VPN - -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. - -### 5. Bad owner or permissions on .ssh - -You tried to run Algo and it quickly exits with an error about a bad owner or permissions: - -``` -fatal: [104.236.2.94]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Bad owner or permissions on /home/user/.ssh/config\r\n", "unreachable": true} -``` - -You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home/user/.ssh` and then `chmod 600 /home/user/.ssh/config`. You may need to repeat this for other files mentioned in the error message. - ### 6. Error: "TypeError: must be str, not bytes" You tried to install Algo and you see many repeated errors referencing `TypeError`, such as `TypeError: '>=' not supported between instances of 'TypeError' and 'int'` and `TypeError: must be str, not bytes`. For example: @@ -87,16 +70,38 @@ fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, " You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.com/ansible/ansible) which has issues with Python3, although this situation is improving over time. Try running Algo with Python2 to fix this issue. Open your terminal and `cd` to the directory with Algo, then run: ``virtualenv -p `which python2.7` env && source env/bin/activate && pip install -r requirements.txt`` -### 7. The region you want is not available - -You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. - ### 8. Error: "ansible-playbook: command not found" You tried to install Algo and you see an error that reads "ansible-playbook: command not found." You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. +### 5. Bad owner or permissions on .ssh + +You tried to run Algo and it quickly exits with an error about a bad owner or permissions: + +``` +fatal: [104.236.2.94]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Bad owner or permissions on /home/user/.ssh/config\r\n", "unreachable": true} +``` + +You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home/user/.ssh` and then `chmod 600 /home/user/.ssh/config`. You may need to repeat this for other files mentioned in the error message. + +### 6. Little Snitch is broken when connected to the VPN + +Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). + +### 7. Various websites appear to be offline through the VPN + +This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. + +### 8. The region you want is not available + +You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. + +### 9. I want to change the list of trusted Wifi networks on my Apple device + +This setting is enforced on your client device via the Apple profile you put on it. You can edit the profile with new settings, then load it on your device to change the settings. You can use the [Apple Configurator](https://itunes.apple.com/us/app/apple-configurator-2/id1037126344?mt=12) to edit and resave the profile. Advanced users can edit the file directly in a text editor. Use the [Configuration Profile Reference](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html) for information about the file format and other available options. If you're not comfortable editing the profile, you can also simply redeploy a new Algo server with different settings to receive a new auto-generated profile. + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel. You may also [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. From c24d51b87b60d072db0c335fe8ce34f8819381e7 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 14:12:23 -0400 Subject: [PATCH 370/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e1c86fa3..282f809a 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -8,7 +8,7 @@ 6. [Little Snitch is broken when connected to the VPN](#6-little-snitch-is-broken-when-connected-to-the-vpn) 7. [Various websites appear to be offline through the VPN](#7-various-websites-appear-to-be-offline-through-the-vpn) 8. [The region you want is not available](#8-the-region-you-want-is-not-available) -9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-Wifi-networks-on-my-Apple-device) +9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) 10. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -58,7 +58,7 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. -### 6. Error: "TypeError: must be str, not bytes" +### 3. Error: "TypeError: must be str, not bytes" You tried to install Algo and you see many repeated errors referencing `TypeError`, such as `TypeError: '>=' not supported between instances of 'TypeError' and 'int'` and `TypeError: must be str, not bytes`. For example: @@ -70,7 +70,7 @@ fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, " You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.com/ansible/ansible) which has issues with Python3, although this situation is improving over time. Try running Algo with Python2 to fix this issue. Open your terminal and `cd` to the directory with Algo, then run: ``virtualenv -p `which python2.7` env && source env/bin/activate && pip install -r requirements.txt`` -### 8. Error: "ansible-playbook: command not found" +### 4. Error: "ansible-playbook: command not found" You tried to install Algo and you see an error that reads "ansible-playbook: command not found." From 4b140bc5edd8cf1a38d4ce0118be01349fabb7a9 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 14:26:35 -0400 Subject: [PATCH 371/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 282f809a..b87dea34 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -104,4 +104,4 @@ This setting is enforced on your client device via the Apple profile you put on ### I have a problem not covered here -If you have an issue that you cannot solve with the guidance here, [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the #tool-algo channel. You may also [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. +If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#tool-algo** channel. From ab22e9aee957333201055d506301a2bd87a88308 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 14:35:30 -0400 Subject: [PATCH 372/769] add note about Apple client support --- docs/TROUBLESHOOTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index b87dea34..b5bcafbe 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -9,7 +9,8 @@ 7. [Various websites appear to be offline through the VPN](#7-various-websites-appear-to-be-offline-through-the-vpn) 8. [The region you want is not available](#8-the-region-you-want-is-not-available) 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) -10. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +10. []() +11. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -102,6 +103,10 @@ You want to install Algo to a specific region in a cloud provider, but that regi This setting is enforced on your client device via the Apple profile you put on it. You can edit the profile with new settings, then load it on your device to change the settings. You can use the [Apple Configurator](https://itunes.apple.com/us/app/apple-configurator-2/id1037126344?mt=12) to edit and resave the profile. Advanced users can edit the file directly in a text editor. Use the [Configuration Profile Reference](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html) for information about the file format and other available options. If you're not comfortable editing the profile, you can also simply redeploy a new Algo server with different settings to receive a new auto-generated profile. +### 10. Error: "The VPN Service payload could not be installed." + +You tried to install the Apple profile on one of your devices and you received an error stating `The "VPN Service" payload could not be installed. The VPN service could not be created.` Client support for Algo VPN is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+. Please upgrade your operating system and try again. + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#tool-algo** channel. From ceca178effb17bf5dd6bf08e89842b56a500760e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 14:36:32 -0400 Subject: [PATCH 373/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index b5bcafbe..68485ad8 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -9,7 +9,7 @@ 7. [Various websites appear to be offline through the VPN](#7-various-websites-appear-to-be-offline-through-the-vpn) 8. [The region you want is not available](#8-the-region-you-want-is-not-available) 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) -10. []() +10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) 11. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From ede472cd369f8f883cc12a42dcf572395a1c8174 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 15:17:26 -0400 Subject: [PATCH 374/769] donations --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2bd6c8ac..e70d61f5 100644 --- a/README.md +++ b/README.md @@ -165,3 +165,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. > Algo is really easy and secure. -- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) + +## Support Algo VPN + +If you want to support Algo VPN we accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](ttps://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks! From 2b0c9c38e2a25bd2b76696e0f99f49ff1a8892da Mon Sep 17 00:00:00 2001 From: Josh Meisels Date: Sat, 1 Apr 2017 12:29:12 -0700 Subject: [PATCH 375/769] Fixed typo (#333) Windows manual steps `-AuthenticationTransformConstants SHA25612 8` had a space that causes this command to fail. Should be `-AuthenticationTransformConstants SHA256128` --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e70d61f5..16be6dbc 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,7 @@ Copy the CA certificate, user certificate, and the user PowerShell script to the If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: -`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA25612 -8 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` +`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu, etc.) From f1cfade2f25852c224976a35ac97e79da77352f4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 15:31:22 -0400 Subject: [PATCH 376/769] referral code --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 16be6dbc..34510d9f 100644 --- a/README.md +++ b/README.md @@ -167,4 +167,8 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Support Algo VPN -If you want to support Algo VPN we accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](ttps://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks! +We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) if you want to support Algo VPN. All the funds we collect go towards continued development. + +Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. It helps support our development costs. + +We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks From 7851cc06efc47bd2e49bcadd6f73ce8939267997 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 15:32:57 -0400 Subject: [PATCH 377/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34510d9f..192609d7 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Support Algo VPN -We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) if you want to support Algo VPN. All the funds we collect go towards continued development. +We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). All donations support continued development. Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. It helps support our development costs. From 1b3a3a886d8186a8bc963dfeaba4c0f0b99cc8cf Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 1 Apr 2017 15:33:24 -0400 Subject: [PATCH 378/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 192609d7..b3e1be80 100644 --- a/README.md +++ b/README.md @@ -171,4 +171,4 @@ We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xc Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. It helps support our development costs. -We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks +We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks! From b8d2dc68bbc469744db568886c1463475f738ee5 Mon Sep 17 00:00:00 2001 From: Matt Mankins Date: Sun, 2 Apr 2017 01:53:53 -0300 Subject: [PATCH 379/769] Change EC2 VPC CIDR blocks to uncommon non-routable addresses (#335) --- roles/cloud-ec2/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 8ef29ce5..8411bef4 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -1,5 +1,5 @@ --- ec2_vpc_nets: - cidr_block: 192.168.0.0/23 - subnet_cidr: 192.168.1.0/24 + cidr_block: 172.16.0.0/12 + subnet_cidr: 172.30.0.0/23 From d37c6b72c5a769c04fc98a32da0bac2571cb1dd1 Mon Sep 17 00:00:00 2001 From: Josh Meisels Date: Sun, 2 Apr 2017 09:34:09 -0700 Subject: [PATCH 380/769] Add new Azure regions and allow user to select VM size (#332) * Update Azure Region List Included several additional regions in the Azure list. In a future version we may want to ask users to choose a continent, then present region options since this list is getting long. * Add VM size selection Added prompt for user to choose VM size. Useful because the default size is not available in all regions, and there are cheaper sizes. * Handle vm_size choice in "Create an Instance" step Use the variable passed in that the user chose for vm_size. * Differentiate Basic A0 and Standard A0 * Remove vm_size D1 since it's being deprecated * Fix syntax issue - missing semicolons * Remove note to self comment * Remove changes to let user select VM size Removing my previous additions that let the user select their Azure VM size. * Hard code VM size to cheapest size Remove my usage of a variable for VM size. Update to use the Basic_A0, which is the cheapest size of VM. --- algo | 27 ++++++++++++++++++++++++++- roles/cloud-azure/tasks/main.yml | 4 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/algo b/algo index ecf33a18..b5d44f8f 100755 --- a/algo +++ b/algo @@ -94,7 +94,7 @@ Name the vpn server: azure_server_name=${azure_server_name:-algo} read -p " - What region should the server be located in? + What region should the server be located in? (https://azure.microsoft.com/en-us/regions/) 1. South Central US 2. Central US 3. North Europe @@ -109,6 +109,19 @@ Name the vpn server: 12. West Central US 13. UK South 14. UK West + 15. West US + 16. Brazil South + 17. Canada Central + 18. Central India + 19. East Asia + 20. Germany Central + 21. Germany Northeast + 22. Korea Central + 23. Korea South + 24. North Central US + 25. South India + 26. West India + Enter the number of your desired region: [1]: " -r azure_region azure_region=${azure_region:-1} @@ -128,6 +141,18 @@ Enter the number of your desired region: 12) region="westcentralus" ;; 13) region="uksouth" ;; 14) region="ukwest" ;; + 15) region="westus" ;; + 16) region="brazilsouth" ;; + 17) region="canadacentral" ;; + 18) region="centralindia" ;; + 19) region="eastasia" ;; + 20) region="germanycentral" ;; + 21) region="germanynortheast" ;; + 22) region="koreacentral" ;; + 23) region="koreasouth" ;; + 24) region="northcentralus" ;; + 25) region="southindia" ;; + 26) region="westindia" ;; esac ROLES="azure vpn cloud" diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index dfdde2e5..17c6ce36 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -80,7 +80,7 @@ virtual_network: algo_net name: "{{ azure_server_name }}" ssh_password_enabled: false - vm_size: Standard_D1 + vm_size: Basic_A0 tags: Environment: Algo ssh_public_keys: @@ -91,6 +91,8 @@ sku: '16.04-LTS' version: latest register: azure_rm_virtualmachine + + # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? - set_fact: ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" From 41ed682213d885e3259ef0729e5f3296e5d767a1 Mon Sep 17 00:00:00 2001 From: James Hale Date: Sun, 2 Apr 2017 15:48:44 -0400 Subject: [PATCH 381/769] Reduce VPC CIDR size to /16 (#341) --- roles/cloud-ec2/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 8411bef4..045fe455 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -1,5 +1,5 @@ --- ec2_vpc_nets: - cidr_block: 172.16.0.0/12 - subnet_cidr: 172.30.0.0/23 + cidr_block: 172.16.0.0/16 + subnet_cidr: 172.16.254.0/23 From 84bbcb88d04ac51af145f9b383498896ad8a9003 Mon Sep 17 00:00:00 2001 From: Josh Soref Date: Sun, 2 Apr 2017 18:14:38 -0500 Subject: [PATCH 382/769] Spelling fixes (#342) * spelling: algorithm * spelling: bertrand * spelling: between * spelling: checking * spelling: conjunction * spelling: contributor * spelling: delimited * spelling: fashion * spelling: droplet * spelling: javascript * spelling: nameserver * spelling: obligatory * spelling: official * spelling: overridden * spelling: overwrite * spelling: parameter * spelling: suppressing --- docs/ADVANCED.md | 2 +- docs/AZURE.md | 2 +- docs/pre-install_redhat_centos_6.x.md | 4 ++-- roles/client/tasks/main.yml | 2 +- roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/dns_adblocking/templates/adblock.sh | 2 +- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 12 ++++++------ roles/proxy/templates/pagespeed.conf.j2 | 12 ++++++------ roles/proxy/templates/privoxy_config.j2 | 2 +- users.yml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index 6b58a10d..e6af53e8 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -15,7 +15,7 @@ git clone https://github.com/trailofbits/algo cd algo && ./algo ``` -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. +**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. ## Scripted Deployment diff --git a/docs/AZURE.md b/docs/AZURE.md index a38278af..909d12e4 100644 --- a/docs/AZURE.md +++ b/docs/AZURE.md @@ -12,7 +12,7 @@ | 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | | 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | | 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | -| 11. Select a role (Contibutor will enough for all)| [![step11-thumb]][step11-screen] | +| 11. Select a role (Contributor will enough for all)| [![step11-thumb]][step11-screen] | | 12. Swith next to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | Now you can use Environment Variables: diff --git a/docs/pre-install_redhat_centos_6.x.md b/docs/pre-install_redhat_centos_6.x.md index 4371a8f6..ca598716 100644 --- a/docs/pre-install_redhat_centos_6.x.md +++ b/docs/pre-install_redhat_centos_6.x.md @@ -19,7 +19,7 @@ Fix GPG key warnings during Ansible rpm install: ``rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6`` -Fix GPG key warning during offical Software Collections (SCL) package install: +Fix GPG key warning during official Software Collections (SCL) package install: ``rpm --import https://raw.githubusercontent.com/sclorg/centos-release-scl/master/centos-release-scl/RPM-GPG-KEY-CentOS-SIG-SCLo`` @@ -33,7 +33,7 @@ yum -y -q install centos-release-SCL yum -y -q install python27-python-devel python27-python-setuptools python27-python-pip yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel wget unzip ansible nano -# Enable 2.7 default for this session (needs re-run beween logins & reboots) +# Enable 2.7 default for this session (needs re-run between logins & reboots) # shellcheck disable=SC1091 source /opt/rh/python27/enable # We're now defaulted to 2.7 diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index ae340d34..9a2fe0aa 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -4,7 +4,7 @@ - name: Include system based facts and tasks include: systems/main.yml -- name: Cheking the signature algorithm +- name: Checking the signature algorithm local_action: > shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 become: no diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 2251f847..a472fb56 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -68,7 +68,7 @@ - set_fact: cloud_instance_ip: "{{ do.droplet.ip_address }}" -- name: Tag the groplet +- name: Tag the droplet digital_ocean_tag: name: "Environment:Algo" resource_id: "{{ do.droplet.id }}" diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh index 46a55d14..864e35ef 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh @@ -31,7 +31,7 @@ awk '{sub(/\r$/,"");print $1,$2}' "$TEMP"|sort -u > "$TEMP_SORTED" #Filter (if applicable) if [ -s "/var/lib/dnsmasq/white.list" ] then - #Filter the blacklist, supressing whitelist matches + #Filter the blacklist, suppressing whitelist matches # This is relatively slow =-( echo 'Filtering white list...' egrep -v "^[[:space:]]*$" /var/lib/dnsmasq/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > /var/lib/dnsmasq/block.hosts diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 69c317e6..026c985e 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -191,7 +191,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # add names to the DNS for the IPv6 address of SLAAC-configured dual-stack # hosts. Use the DHCPv4 lease to derive the name, network segment and # MAC address and assume that the host will also have an -# IPv6 address calculated using the SLAAC alogrithm. +# IPv6 address calculated using the SLAAC algorithm. #dhcp-range=1234::, ra-names # Do Router Advertisements, BUT NOT DHCP for this subnet. @@ -212,7 +212,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts #dhcp-range=1234::, ra-stateless, ra-names # Do router advertisements for all subnets where we're doing DHCPv6 -# Unless overriden by ra-stateless, ra-names, et al, the router +# Unless overridden by ra-stateless, ra-names, et al, the router # advertisements will have the M and O bits set, so that the clients # get addresses and configuration from DHCPv6, and the A bit reset, so the # clients don't use SLAAC addresses. @@ -290,7 +290,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # Give a fixed IPv6 address and name to client with # DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 # Note the MAC addresses CANNOT be used to identify DHCPv6 clients. -# Note also the they [] around the IPv6 address are obilgatory. +# Note also the they [] around the IPv6 address are obligatory. #dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] # Ignore any clients which are not specified in dhcp-host lines @@ -347,7 +347,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # Send DHCPv6 option. Note [] around IPv6 addresses. #dhcp-option=option6:dns-server,[1234::77],[1234::88] -# Send DHCPv6 option for namservers as the machine running +# Send DHCPv6 option for nameservers as the machine running # dnsmasq and another. #dhcp-option=option6:dns-server,[::],[1234::88] @@ -527,7 +527,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # (using /etc/hosts) then that name can be specified as the # tftp_servername (the third option to dhcp-boot) and in that # case dnsmasq resolves this name and returns the resultant IP -# addresses in round robin fasion. This facility can be used to +# addresses in round robin fashion. This facility can be used to # load balance the tftp load among a set of servers. #dhcp-boot=/var/ftpd/pxelinux.0,boothost,tftp_server_name @@ -648,7 +648,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # Provide an alias for a "local" DNS name. Note that this _only_ works # for targets which are names from DHCP or /etc/hosts. Give host # "bert" another name, bertrand -#cname=bertand,bert +#cname=bertrand,bert # For debugging purposes, log each DNS query as it passes through # dnsmasq. diff --git a/roles/proxy/templates/pagespeed.conf.j2 b/roles/proxy/templates/pagespeed.conf.j2 index 3b89b758..026a6864 100644 --- a/roles/proxy/templates/pagespeed.conf.j2 +++ b/roles/proxy/templates/pagespeed.conf.j2 @@ -62,7 +62,7 @@ ModPagespeedEnableFilters lazyload_images # Explicitly disables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, if one + # conjunction with ModPagespeedRewriteLevel. For instance, if one # of the filters in the CoreFilters needs to be disabled for a # site, that filter can be added to # ModPagespeedDisableFilters. This directive contains a @@ -71,7 +71,7 @@ # ModPagespeedDisableFilters rewrite_images # Explicitly enables specific filters. This is useful in - # conjuction with ModPagespeedRewriteLevel. For instance, filters + # conjunction with ModPagespeedRewriteLevel. For instance, filters # not included in the CoreFilters may be enabled using this # directive. This directive contains a comma-separated list of # filter names, and can be repeated. @@ -147,7 +147,7 @@ # ModPagespeedMaxCombinedJsBytes 92160 # Limit the number of inodes in the file cache. Set to 0 for no limit. - # The default value if this paramater is not specified is 0 (no limit). + # The default value if this parameter is not specified is 0 (no limit). ModPagespeedFileCacheInodeLimit 500000 # Bound the number of images that can be rewritten at any one time; this @@ -181,7 +181,7 @@ # ModPagespeedRewriteRandomDropPercentage 90 # Many filters modify the URLs of resources in HTML files. This is typically - # harmless but pages whose Javascript expects to read or modify the original + # harmless but pages whose JavaScript expects to read or modify the original # URLs may break. The following parameters prevent filters from modifying # URLs of their respective types. # @@ -267,7 +267,7 @@ # # You can uncomment this to let mod_pagespeed rename all JS files. # - # ModPagespeedAvoidRenamingIntrospectiveJavascript off + # ModPagespeedAvoidRenamingIntrospectiveJavaScript off # Certain common JavaScript libraries are available from Google, which acts # as a CDN and allows you to benefit from browser caching if a new visitor @@ -305,7 +305,7 @@ # Enables server-side instrumentation and statistics. If this rewriter is - # enabled, then each rewritten HTML page will have instrumentation javacript + # enabled, then each rewritten HTML page will have instrumentation javascript # added that sends latency beacons to /mod_pagespeed_beacon. These # statistics can be accessed at /mod_pagespeed_statistics. You must also # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers diff --git a/roles/proxy/templates/privoxy_config.j2 b/roles/proxy/templates/privoxy_config.j2 index 485734cc..dcbe3484 100644 --- a/roles/proxy/templates/privoxy_config.j2 +++ b/roles/proxy/templates/privoxy_config.j2 @@ -998,7 +998,7 @@ enforce-blocks 0 # whole destination part are optional. # # If your system implements RFC 3493, then src_addr and dst_addr -# can be IPv6 addresses delimeted by brackets, port can be a +# can be IPv6 addresses delimited by brackets, port can be a # number or a service name, and src_masklen and dst_masklen can # be a number from 0 to 128. # diff --git a/users.yml b/users.yml index 314858dc..421c5814 100644 --- a/users.yml +++ b/users.yml @@ -51,7 +51,7 @@ - name: Gather Facts setup: - - name: Cheking the signature algorithm + - name: Checking the signature algorithm local_action: > shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 become: no From f9f1d5cd38b7b2f486decef660340e810a53dce7 Mon Sep 17 00:00:00 2001 From: Dana Klassen Date: Sun, 2 Apr 2017 22:17:17 -0400 Subject: [PATCH 383/769] Typo cheking to checking (#344) From 050583ecd1ee2fc1e58f66d3598bb7eb26bf05f6 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 3 Apr 2017 15:33:15 -0400 Subject: [PATCH 384/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3e1be80..7ec99814 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) [![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) -Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. +Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. ## Features From 1c4659311a2c9d280afbd075d91ab235c02f73c8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 3 Apr 2017 15:43:32 -0400 Subject: [PATCH 385/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41b0be90..1d038986 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,10 @@ ### Filing New Issues -* Check that your issue is not already described in the [FAQ](docs/FAQ.md) or [troubleshooting](docs/TROUBLESHOOTING.md) docs +* Check that your issue is not already described in the [FAQ](docs/FAQ.md), [troubleshooting](docs/TROUBLESHOOTING.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) * Did you remember to install the dependencies for your operating system prior to installing Algo? * Client support is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. -* If you need to file an issue, fill out any relevant fields in the Issue Template +* Cloud provider support is limited to DO, AWS, GCE, and Azure. Any others are best effort only. +* If you need to file a new issue, fill out any relevant fields in the Issue Template ### Pull Requests From 2fc9122ae8a1397a081df694934fc7864996448e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 3 Apr 2017 16:53:04 -0400 Subject: [PATCH 386/769] add router troubleshooting faq --- docs/TROUBLESHOOTING.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 68485ad8..97ad5bf7 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -10,7 +10,8 @@ 8. [The region you want is not available](#8-the-region-you-want-is-not-available) 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) 10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) -11. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-my-algo-server) +12. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -106,7 +107,11 @@ This setting is enforced on your client device via the Apple profile you put on ### 10. Error: "The VPN Service payload could not be installed." You tried to install the Apple profile on one of your devices and you received an error stating `The "VPN Service" payload could not be installed. The VPN service could not be created.` Client support for Algo VPN is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+. Please upgrade your operating system and try again. - + +### 11. I can't get my router to connect to the Algo server + +In order to connect to the Algo VPN server, your router must support IKEv2, ECC certificate-based authentication, and the cipher suite we use. See the ipsec.conf files we generate in the `config` folder for more information. Note that we do not officially support routers as clients for Algo VPN at this time, though patches and documentation for them are welcome (for example, see open issues for [Ubiquiti](https://github.com/trailofbits/algo/issues/307) and [pfSense](https://github.com/trailofbits/algo/issues/292)). + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#tool-algo** channel. From fb09a4138ffd6efd57db8d233d371abf54a22b5a Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 3 Apr 2017 16:54:20 -0400 Subject: [PATCH 387/769] Update TROUBLESHOOTING.md --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 97ad5bf7..9b1ce36e 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -10,7 +10,7 @@ 8. [The region you want is not available](#8-the-region-you-want-is-not-available) 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) 10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) -11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-my-algo-server) +11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-the-algo-server) 12. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From b8cfb98c5d1d13d600986caa249225abfe887668 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 4 Apr 2017 16:20:10 +0200 Subject: [PATCH 388/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d3775717..bfcf0f07 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,7 +7,10 @@ ### Version of components from `requirements.txt` - +``` +pip show (all the packages from requirements.txt) +PUT THE OUTPUT HERE. DON'T NEED TO PASTE requirements.txt +``` From eae43536919f3bf52f7bd49cf3c424a0d5c50801 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 4 Apr 2017 16:23:51 +0200 Subject: [PATCH 389/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bfcf0f07..83dd269f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ ### OS / Environment - + ### Ansible version @@ -13,7 +13,6 @@ PUT THE OUTPUT HERE. DON'T NEED TO PASTE requirements.txt ``` - ### Summary of the problem @@ -35,5 +34,5 @@ PUT THE OUTPUT HERE. DON'T NEED TO PASTE requirements.txt ### Full log - + From 578bb3344d27b6ea2059c719e0e972d9ca490deb Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 4 Apr 2017 16:37:47 +0200 Subject: [PATCH 390/769] Fixes #314 --- algo | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/algo b/algo index b5d44f8f..4ec38f8b 100755 --- a/algo +++ b/algo @@ -121,7 +121,7 @@ Name the vpn server: 24. North Central US 25. South India 26. West India - + Enter the number of your desired region: [1]: " -r azure_region azure_region=${azure_region:-1} @@ -239,11 +239,11 @@ Name the vpn server: 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) 12. eu-west-2 EU (London) - 13. sa-east-1 South America (São Paulo) - 14. ca-central-1 Canada (Central) + 13. ca-central-1 Canada (Central) Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} + # sa-east-1 region does not support the size instance we use. case "$aws_region" in 1) region="us-east-1" ;; @@ -257,9 +257,8 @@ Enter the number of your desired region: 9) region="ap-northeast-1" ;; 10) region="eu-central-1" ;; 11) region="eu-west-1" ;; - 12) region="eu-west-2";; - 13) region="sa-east-1" ;; - 14) region="ca-central-1" ;; + 12) region="eu-west-2";; + 13) region="ca-central-1" ;; esac ROLES="ec2 vpn cloud" From c0f4b5fa41db8c75d5e383d934381933443f29fb Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 4 Apr 2017 16:57:39 +0200 Subject: [PATCH 391/769] Enable default values if the role is skipped #313 --- roles/common/tasks/freebsd.yml | 5 ++--- roles/common/tasks/main.yml | 4 ++-- roles/common/tasks/ubuntu.yml | 7 +++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index bf861084..08c04a1e 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -10,9 +10,8 @@ - bash - wget sysctl: - forwarding: - - net.inet.ip.forwarding - - net.inet6.ip6.forwarding + - net.inet.ip.forwarding + - net.inet6.ip6.forwarding tags: - always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index d8f6ec3e..68ca4d40 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -14,14 +14,14 @@ - name: Install tools package: name="{{ item }}" state=present with_items: - - "{{ tools }}" + - "{{ tools|default([]) }}" tags: - always - name: Enable packet forwarding for IPv4 sysctl: name="{{ item }}" value=1 with_items: - - "{{ sysctl.forwarding }}" + - "{{ sysctl|default([]) }}" tags: - always diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index e8309939..ada74f43 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -83,9 +83,8 @@ - cgroup-tools - openssl sysctl: - forwarding: - - net.ipv4.ip_forward - - net.ipv4.conf.all.forwarding - - net.ipv6.conf.all.forwarding + - net.ipv4.ip_forward + - net.ipv4.conf.all.forwarding + - net.ipv6.conf.all.forwarding tags: - always From 6e61a51aca268034446bdc942eb77fea69516b54 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 4 Apr 2017 17:02:11 +0200 Subject: [PATCH 392/769] rewrite the sysctl task --- roles/common/tasks/freebsd.yml | 6 ++++-- roles/common/tasks/main.yml | 4 ++-- roles/common/tasks/ubuntu.yml | 9 ++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 08c04a1e..67d247d8 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -10,8 +10,10 @@ - bash - wget sysctl: - - net.inet.ip.forwarding - - net.inet6.ip6.forwarding + - item: net.inet.ip.forwarding + value: 1 + - item: net.inet6.ip6.forwarding + value: 1 tags: - always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 68ca4d40..8c8a9933 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -18,8 +18,8 @@ tags: - always -- name: Enable packet forwarding for IPv4 - sysctl: name="{{ item }}" value=1 +- name: Sysctl tuning + sysctl: name="{{ item.item }}" value="{{ item.value }}" with_items: - "{{ sysctl|default([]) }}" tags: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index ada74f43..b512af61 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -83,8 +83,11 @@ - cgroup-tools - openssl sysctl: - - net.ipv4.ip_forward - - net.ipv4.conf.all.forwarding - - net.ipv6.conf.all.forwarding + - item: net.ipv4.ip_forward + value: 1 + - item: net.ipv4.conf.all.forwarding + value: 1 + - item: net.ipv6.conf.all.forwarding + value: 1 tags: - always From 8c6f1f9c696696c0120e359f5893924d93f2ae9f Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 4 Apr 2017 16:20:48 -0400 Subject: [PATCH 393/769] add note about network manager on older Ubuntu --- docs/TROUBLESHOOTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 9b1ce36e..d8d1ca20 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -11,7 +11,8 @@ 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) 10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) 11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-the-algo-server) -12. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +12. [I can't get Network Manager to connect to the Algo Server)[#12-i-cant-get-network-manager-to-connect-to-the-algo-server) +13. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -112,6 +113,10 @@ You tried to install the Apple profile on one of your devices and you received a In order to connect to the Algo VPN server, your router must support IKEv2, ECC certificate-based authentication, and the cipher suite we use. See the ipsec.conf files we generate in the `config` folder for more information. Note that we do not officially support routers as clients for Algo VPN at this time, though patches and documentation for them are welcome (for example, see open issues for [Ubiquiti](https://github.com/trailofbits/algo/issues/307) and [pfSense](https://github.com/trailofbits/algo/issues/292)). +### 12. I can't get Network Manager to connect to the Algo server + +You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263] without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#tool-algo** channel. From 3a511eab44e1794b6d1368657e25b141b8aa103d Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 4 Apr 2017 16:21:31 -0400 Subject: [PATCH 394/769] I suck at making this stupid TOC :-x --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index d8d1ca20..1063b20e 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -11,7 +11,7 @@ 9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) 10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) 11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-the-algo-server) -12. [I can't get Network Manager to connect to the Algo Server)[#12-i-cant-get-network-manager-to-connect-to-the-algo-server) +12. [I can't get Network Manager to connect to the Algo Server](#12-i-cant-get-network-manager-to-connect-to-the-algo-server) 13. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" From c2ecab3f98a5a6ef4c885f2710243428eed6304e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 4 Apr 2017 16:22:57 -0400 Subject: [PATCH 395/769] another typo ugh --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 1063b20e..4798f866 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -115,7 +115,7 @@ In order to connect to the Algo VPN server, your router must support IKEv2, ECC ### 12. I can't get Network Manager to connect to the Algo server -You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263] without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. +You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263) without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. ### I have a problem not covered here From 5a172eb3ec24b2754b788681e99a629f2880872e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 4 Apr 2017 16:50:15 -0400 Subject: [PATCH 396/769] new Slack channel --- docs/TROUBLESHOOTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 4798f866..8c4c0358 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -119,4 +119,4 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network ### I have a problem not covered here -If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#tool-algo** channel. +If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#algo-support** channel. From 27680b9403c4217f2baa51196ec869458f6c47fe Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 5 Apr 2017 16:14:11 +0200 Subject: [PATCH 397/769] add msrestazure to the requirements #269 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d5799fb9..67ec4a10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +msrestazure setuptools>=11.3 ansible>=2.1,<2.2.1 dopy==0.3.5 From 3b8d04d06cb0ecd6ce2b788fa963394bf37ccabd Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 5 Apr 2017 16:25:56 +0200 Subject: [PATCH 398/769] remove the logging role --- deploy.yml | 1 - docs/ADVANCED.md | 2 - docs/ROLES.md | 3 - roles/logging/handlers/main.yml | 5 -- roles/logging/meta/main.yml | 4 - roles/logging/tasks/main.yml | 35 -------- roles/logging/templates/CIS.conf.j2 | 15 ---- roles/logging/templates/audit.rules.j2 | 101 ------------------------ roles/logging/templates/auditd.conf.j2 | 32 -------- roles/logging/templates/rsyslog.conf.j2 | 61 -------------- 10 files changed, 259 deletions(-) delete mode 100644 roles/logging/handlers/main.yml delete mode 100644 roles/logging/meta/main.yml delete mode 100644 roles/logging/tasks/main.yml delete mode 100644 roles/logging/templates/CIS.conf.j2 delete mode 100644 roles/logging/templates/audit.rules.j2 delete mode 100644 roles/logging/templates/auditd.conf.j2 delete mode 100644 roles/logging/templates/rsyslog.conf.j2 diff --git a/deploy.yml b/deploy.yml index c1adc4b8..3d6966e1 100644 --- a/deploy.yml +++ b/deploy.yml @@ -49,7 +49,6 @@ - { role: security, tags: [ 'security' ] } - { role: proxy, tags: [ 'proxy', 'adblock' ] } - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - - { role: logging, tags: [ 'logging' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - { role: vpn, tags: [ 'vpn' ] } diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index e6af53e8..8a7508f0 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -48,7 +48,6 @@ Server roles: - role: vpn, tags: vpn - role: dns_adblocking, tags: dns, adblock - role: proxy, tags: proxy, adblock -- role: logging, tags: logging - role: security, tags: security - role: ssh_tunneling, tags: ssh_tunneling @@ -117,7 +116,6 @@ Possible options for `region`: - eu-central-1 - eu-west-1 - eu-west-2 -- sa-east-1 Additional tags: diff --git a/docs/ROLES.md b/docs/ROLES.md index f43e6f8d..5c90007c 100644 --- a/docs/ROLES.md +++ b/docs/ROLES.md @@ -24,9 +24,6 @@ * **DNS-based Adblocking** * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations -* **Security Monitoring and Logging** - * Configures [auditd](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Security_Guide/chap-system_auditing.html) and rsyslog to log data useful for investigating security incidents - * Sends logs to a configured email address on a regular basis * **SSH Tunneling** * Adds a restricted `algo` group with no shell access and limited SSH forwarding options * Creates one limited, local account per user and an SSH public key for each diff --git a/roles/logging/handlers/main.yml b/roles/logging/handlers/main.yml deleted file mode 100644 index 9dcd122d..00000000 --- a/roles/logging/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: restart rsyslog - service: name=rsyslog state=restarted - -- name: restart auditd - service: name=auditd state=restarted diff --git a/roles/logging/meta/main.yml b/roles/logging/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/logging/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/logging/tasks/main.yml b/roles/logging/tasks/main.yml deleted file mode 100644 index 467de88b..00000000 --- a/roles/logging/tasks/main.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Auditd - -- name: Auditd installed - apt: name=auditd state=latest - -- name: Auditd rules configured - template: src=audit.rules.j2 dest=/etc/audit/audit.rules - notify: - - restart auditd - -- name: Auditd configured - template: src=auditd.conf.j2 dest=/etc/audit/auditd.conf - notify: - - restart auditd - -- name: Enable services - service: name=auditd enabled=yes - -# Rsyslog - -- name: Rsyslog installed - apt: name=rsyslog state=latest - -- name: Rsyslog configured - template: src=rsyslog.conf.j2 dest=/etc/rsyslog.conf - notify: - - restart rsyslog - -- name: Rsyslog CIS configured - template: src=CIS.conf.j2 dest=/etc/rsyslog.d/CIS.conf owner=root group=root mode=0644 - notify: - - restart rsyslog - -- name: Enable services - service: name=rsyslog enabled=yes diff --git a/roles/logging/templates/CIS.conf.j2 b/roles/logging/templates/CIS.conf.j2 deleted file mode 100644 index 96b3a595..00000000 --- a/roles/logging/templates/CIS.conf.j2 +++ /dev/null @@ -1,15 +0,0 @@ -*.emerg :omusrmsg:* -mail.* -/var/log/mail -mail.info -/var/log/mail.info -mail.warning -/var/log/mail.warn -mail.err /var/log/mail.err -news.crit -/var/log/news/news.crit -news.err -/var/log/news/news.err -news.notice -/var/log/news/news.notice -*.=warning;*.=err -/var/log/warn -*.crit /var/log/warn -*.*;mail.none;news.none -/var/log/messages -local0,local1.* -/var/log/localmessages -local2,local3.* -/var/log/localmessages -local4,local5.* -/var/log/localmessages -local6,local7.* -/var/log/localmessages \ No newline at end of file diff --git a/roles/logging/templates/audit.rules.j2 b/roles/logging/templates/audit.rules.j2 deleted file mode 100644 index 3464e2a1..00000000 --- a/roles/logging/templates/audit.rules.j2 +++ /dev/null @@ -1,101 +0,0 @@ -# This file contains the auditctl rules that are loaded -# whenever the audit daemon is started via the initscripts. -# The rules are simply the parameters that would be passed -# to auditctl. -# -# First rule - delete all --D - -# Increase the buffers to survive stress events. -# Make this bigger for busy systems --b 320 - -# Feel free to add below this line. See auditctl man page - -# Record Events That Modify Date and Time Information -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S clock_settime -k time-change --a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change -{% endif %} --a always,exit -F arch=b32 -S clock_settime -k time-change --a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change --w /etc/localtime -p wa -k time-change - -# Record Events That Modify User/Group Information --w /etc/group -p wa -k identity --w /etc/passwd -p wa -k identity --w /etc/gshadow -p wa -k identity --w /etc/shadow -p wa -k identity --w /etc/security/opasswd -p wa -k identity - -# Record Events That Modify the System's Network Environment -{% if ansible_architecture == "x86_64" %} --a exit,always -F arch=b64 -S sethostname -S setdomainname -k system-locale -{% endif %} --a exit,always -F arch=b32 -S sethostname -S setdomainname -k system-locale --w /etc/issue -p wa -k system-locale --w /etc/issue.net -p wa -k system-locale --w /etc/hosts -p wa -k system-locale --w /etc/network/interfaces -p wa -k system-locale - -# Collect Login and Logout Events --w /var/log/faillog -p wa -k logins --w /var/log/lastlog -p wa -k logins --w /var/log/tallylog -p wa -k logins - -# Collect Session Initiation Information --w /var/run/utmp -p wa -k session --w /var/log/wtmp -p wa -k session --w /var/log/btmp -p wa -k session - -# Collect Discretionary Access Control Permission Modification Events -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod -{% endif %} --a always,exit -F arch=b32 -S chmod -S fchmod -S fchmodat -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b32 -S chown -S fchown -S fchownat -S lchown -F auid>=500 -F auid!=4294967295 -k perm_mod --a always,exit -F arch=b32 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=500 -F auid!=4294967295 -k perm_mod - -# Collect Unsuccessful Unauthorized Access Attempts to Files -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access --a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access -{% endif %} --a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=500 -F auid!=4294967295 -k access --a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=500 -F auid!=4294967295 -k access - -# Collect Use of Privileged Commands -{% if privileged_programs is defined and privileged_programs.stdout_lines|length > 0 %} -{{ privileged_programs.stdout }} -{% endif %} - -# Collect Successful File System Mounts -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S mount -F auid>=500 -F auid!=4294967295 -k mounts -{% endif %} --a always,exit -F arch=b32 -S mount -F auid>=500 -F auid!=4294967295 -k mounts - -# Collect File Deletion Events by User -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete -{% endif %} --a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -F auid>=500 -F auid!=4294967295 -k delete - -# Collect Changes to System Administration Scope --w /etc/sudoers -p wa -k scope - -# Collect System Administrator Actions (sudolog) --w /var/log/sudo.log -p wa -k actions - -# Collect Kernel Module Loading and Unloading -{% if ansible_architecture == "x86_64" %} --a always,exit -F arch=b64 -S init_module -S delete_module -k modules -{% endif %} --a always,exit -F arch=b32 -S init_module -S delete_module -k modules --w /sbin/insmod -p x -k modules --w /sbin/rmmod -p x -k modules --w /sbin/modprobe -p x -k modules - --e 2 diff --git a/roles/logging/templates/auditd.conf.j2 b/roles/logging/templates/auditd.conf.j2 deleted file mode 100644 index 24aac738..00000000 --- a/roles/logging/templates/auditd.conf.j2 +++ /dev/null @@ -1,32 +0,0 @@ -# -# This file controls the configuration of the audit daemon -# - -log_file = /var/log/audit/audit.log -log_format = RAW -log_group = root -priority_boost = 4 -flush = INCREMENTAL -freq = 20 -num_logs = 5 -disp_qos = lossy -dispatcher = /sbin/audispd -name_format = NONE -##name = mydomain -max_log_file = 10 -max_log_file_action = keep_logs -space_left = 75 -space_left_action = email -action_mail_acct = {{ auditd_action_mail_acct }} -admin_space_left = 50 -admin_space_left_action = email -disk_full_action = SUSPEND -disk_error_action = SUSPEND -##tcp_listen_port = -tcp_listen_queue = 5 -tcp_max_per_addr = 1 -##tcp_client_ports = 1024-65535 -tcp_client_max_idle = 0 -enable_krb5 = no -krb5_principal = auditd -##krb5_key_file = /etc/audit/audit.key \ No newline at end of file diff --git a/roles/logging/templates/rsyslog.conf.j2 b/roles/logging/templates/rsyslog.conf.j2 deleted file mode 100644 index 25513801..00000000 --- a/roles/logging/templates/rsyslog.conf.j2 +++ /dev/null @@ -1,61 +0,0 @@ -# /etc/rsyslog.conf Configuration file for rsyslog. -# -# For more information see -# /usr/share/doc/rsyslog-doc/html/rsyslog_conf.html -# -# Default logging rules can be found in /etc/rsyslog.d/50-default.conf - -# -################# -#### MODULES #### -################# - -module(load="imuxsock") # provides support for local system logging -module(load="imklog") # provides kernel logging support -#module(load="immark") # provides --MARK-- message capability - -# provides UDP syslog reception -#module(load="imudp") -#input(type="imudp" port="514") - -# provides TCP syslog reception -#module(load="imtcp") -#input(type="imtcp" port="514") - -# Enable non-kernel facility klog messages -$KLogPermitNonKernelFacility on - -########################### -#### GLOBAL DIRECTIVES #### -########################### - -# -# Use traditional timestamp format. -# To enable high precision timestamps, comment out the following line. -# -$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat - -# Filter duplicated messages -$RepeatedMsgReduction on - -# -# Set the default permissions for all log files. -# -$FileOwner syslog -$FileGroup adm -$FileCreateMode 0640 -$DirCreateMode 0755 -$Umask 0022 -$PrivDropToUser syslog -$PrivDropToGroup syslog - -# -# Where to place spool and state files -# -$WorkDirectory /var/spool/rsyslog - -# -# Include all config files in /etc/rsyslog.d/ -# -$IncludeConfig /etc/rsyslog.d/*.conf - From 68126537c96fb3975fedf559805b878052f61874 Mon Sep 17 00:00:00 2001 From: Josh Tamayo Date: Wed, 5 Apr 2017 10:56:26 -0400 Subject: [PATCH 399/769] Update Powershell Instructions for VPN connection (#350). Fixes #329 * Update Powershell Instructions for VPN connection Update the Powershell instructions for creating the Windows VPN connection. Clarfied requirement that Powershell run as Administrator, and additional steps required to update the Execution Policies for PowerShell. * Formatting updates Cleaned up capitalization and spacing errors, as well as improved wording a bit. * Missed an S Evil, evil capitalization. --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ec99814..3fcae311 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,15 @@ You need to install the [strongSwan VPN Client for Android 4 and newer](https:// ### Windows -Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. +Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. +The PowerShell script has to be run as Administrator, so first open PowerShell as Administrator, then navigate to your copied files. If you have never used PowerShell before, you will need to change the Execution Policy to allow unsigned scripts to run. Execute the following command in PowerShell to do so. +```powershell +Set-ExecutionPolicy Unrestricted -Scope CurrentUser +``` +After you execute the user script remember to revert the policy change before you close the PowerShell window. +```powershell +Set-ExecutionPolicy Restricted -Scope CurrentUser +``` If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: From 3df33c0eba872aa1fa1e79b5378c9b885a231783 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 5 Apr 2017 17:08:52 +0200 Subject: [PATCH 400/769] Add a comment about escaping usernames --- config.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/config.cfg b/config.cfg index d4e9a3d3..31803f79 100644 --- a/config.cfg +++ b/config.cfg @@ -2,6 +2,7 @@ # Add as many users as you want for your VPN server here. # Access credentials will be generated for each one. +# Don't forget to escape nicknames with quotes, if you are using zero in nicknames. users: - dan - jack From 1af2010f44444c8eb9cf8e497078da00632155e7 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 5 Apr 2017 14:31:31 -0400 Subject: [PATCH 401/769] Update config.cfg --- config.cfg | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/config.cfg b/config.cfg index 31803f79..747bae5f 100644 --- a/config.cfg +++ b/config.cfg @@ -1,15 +1,12 @@ --- # Add as many users as you want for your VPN server here. -# Access credentials will be generated for each one. -# Don't forget to escape nicknames with quotes, if you are using zero in nicknames. +# Credentials will be generated for each one. users: - dan - jack -# Add an email address to send logs if you're using auditd for monitoring. -# Avoid using '+' in your email address otherwise auditd will fail to start. -auditd_action_mail_acct: email@example.com +# NOTE: If your usernames have leading 0's, like "000dan", you have to escape them ### Advanced users only below this line ### From 7bde06309f690ea353d8a99f29e932c38192ada7 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 5 Apr 2017 17:23:18 -0400 Subject: [PATCH 402/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d038986..8fd2698a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ * Check that your issue is not already described in the [FAQ](docs/FAQ.md), [troubleshooting](docs/TROUBLESHOOTING.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) * Did you remember to install the dependencies for your operating system prior to installing Algo? -* Client support is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 16.04+, etc. +* Client support is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 17.04+, etc. * Cloud provider support is limited to DO, AWS, GCE, and Azure. Any others are best effort only. * If you need to file a new issue, fill out any relevant fields in the Issue Template From 8b977afd99405102b43fcd29334e1accc53cd62f Mon Sep 17 00:00:00 2001 From: Casey Lang Date: Fri, 7 Apr 2017 09:51:30 -0500 Subject: [PATCH 403/769] Modify creation of GCE Instance (#363) Update deprecated GCE metadata options --- roles/cloud-gce/tasks/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 9339113c..55e280a1 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -15,10 +15,10 @@ zone: "{{ zone }}" machine_type: f1-micro image: ubuntu-1604 - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" - metadata: '{"sshKeys":"root:{{ ssh_public_key_lookup }}"}' + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' tags: - "environment-algo" register: google_vm From fde3214ecd1aea403e9f94d2cb7406fbe0138195 Mon Sep 17 00:00:00 2001 From: defunct Date: Fri, 7 Apr 2017 15:36:34 -0400 Subject: [PATCH 404/769] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3fcae311..14f48bbf 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) [![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) +[![Bountysource](https://img.shields.io/bountysource/team/mozilla-core/activity.svg)](https://www.bountysource.com/teams/trailofbits) Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. From b918fad669a97d02a7e4c6e403b28fd2014fac76 Mon Sep 17 00:00:00 2001 From: defunct Date: Fri, 7 Apr 2017 15:38:30 -0400 Subject: [PATCH 405/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14f48bbf..3c3e2cac 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) [![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) [![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) -[![Bountysource](https://img.shields.io/bountysource/team/mozilla-core/activity.svg)](https://www.bountysource.com/teams/trailofbits) +[![Bountysource](https://img.shields.io/bountysource/team/trailofbits/activity.svg)](https://www.bountysource.com/teams/trailofbits) Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. From 7214f41cfc44dba755449e5b64952fe36790f4c8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 8 Apr 2017 10:06:54 +0200 Subject: [PATCH 406/769] additional prompts #289 --- algo | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/algo b/algo index 4ec38f8b..817560dd 100755 --- a/algo +++ b/algo @@ -3,6 +3,7 @@ set -e SKIP_TAGS="_null encrypted" +ADDITIONAL_PROMPT="[pasted values will not be displayed]" additional_roles () { @@ -68,32 +69,38 @@ azure () { read -p " Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +$ADDITIONAL_PROMPT [...]: " -rs azure_secret read -p " Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +$ADDITIONAL_PROMPT [...]: " -rs azure_tenant read -p " Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +$ADDITIONAL_PROMPT [...]: " -rs azure_client_id read -p " Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials +$ADDITIONAL_PROMPT [...]: " -rs azure_subscription_id read -p " + Name the vpn server: [algo]: " -r azure_server_name azure_server_name=${azure_server_name:-algo} read -p " + What region should the server be located in? (https://azure.microsoft.com/en-us/regions/) 1. South Central US 2. Central US @@ -161,16 +168,18 @@ Enter the number of your desired region: digitalocean () { read -p " -Enter your API token (https://cloud.digitalocean.com/settings/api/tokens): -[pasted values will not be displayed] +Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): +$ADDITIONAL_PROMPT : " -rs do_access_token read -p " + Name the vpn server: [algo.local]: " -r do_server_name do_server_name=${do_server_name:-algo.local} read -p " + What region should the server be located in? 1. Amsterdam (Datacenter 2) 2. Amsterdam (Datacenter 3) @@ -211,21 +220,24 @@ ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached -[pasted values will not be displayed] +$ADDITIONAL_PROMPT [AKIA...]: " -rs aws_access_key read -p " + Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached -[pasted values will not be displayed] +$ADDITIONAL_PROMPT [ABCD...]: " -rs aws_secret_key read -p " + Name the vpn server: [algo]: " -r aws_server_name aws_server_name=${aws_server_name:-algo} read -p " + What region should the server be located in? 1. us-east-1 US East (N. Virginia) 2. us-east-2 US East (Ohio) @@ -271,11 +283,13 @@ Enter the local path to your credentials JSON file (https://support.google.com/c : " -r credentials_file read -p " + Name the vpn server: [algo]: " -r server_name server_name=${server_name:-algo} read -p " + What zone should the server be located in? 1. Western US (Oregon A) 2. Western US (Oregon B) @@ -324,11 +338,13 @@ Enter the IP address of your server: (or use localhost for local installation) : " -r server_ip read -p " + What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) [root]: " -r server_user server_user=${server_user:-root} read -p " + Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) [$server_ip]: " -r IP_subject IP_subject=${IP_subject:-$server_ip} @@ -338,6 +354,7 @@ Enter the public IP address of your server: (IMPORTANT! This IP is used to verif SKIP_TAGS+=" cloud update-alternatives" read -p " + Was this server deployed by Algo previously? [y/N]: " -r Deployed_By_Algo Deployed_By_Algo=${Deployed_By_Algo:-n} @@ -390,11 +407,12 @@ ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} read -p " Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -: " -r IP_subject +[$server_ip]: " -r IP_subject + IP_subject=${IP_subject:-$server_ip} read -p " Enter the password for the private CA key: -[pasted values will not be displayed] +$ADDITIONAL_PROMPT : " -rs easyrsa_CA_password ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" From 47515154bb39f99795531816b83c328e8a83e908 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 8 Apr 2017 10:39:04 +0200 Subject: [PATCH 407/769] add mtu in the sswan profile --- roles/vpn/templates/sswan.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/sswan.j2 b/roles/vpn/templates/sswan.j2 index 1c2a87a9..4fa4fb84 100644 --- a/roles/vpn/templates/sswan.j2 +++ b/roles/vpn/templates/sswan.j2 @@ -1,11 +1,12 @@ { "uuid": "{{ 600000 | random | to_uuid }}", - "name": "Algo VPN {{ IP_subject_alt_name }}", + "name": "Algo {{ IP_subject_alt_name }}", "type": "ikev2-cert", "remote": { "addr": "{{ IP_subject_alt_name }}" }, "local": { "p12": "{{ item.1.stdout }}" - } + }, + "mtu": 1280 } From 495b37737a336dbc8fdc1c338f2697a2ca93871e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 09:44:52 -0400 Subject: [PATCH 408/769] add GCE warning and explicit Ubuntu version --- algo | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/algo b/algo index 817560dd..525064f0 100755 --- a/algo +++ b/algo @@ -367,9 +367,9 @@ algo_provisioning () { What provider would you like to use? 1. DigitalOcean 2. Amazon EC2 - 3. Google Compute Engine - 4. Microsoft Azure - 5. Install to existing Ubuntu server + 3. Microsoft Azure + 4. Google Compute Engine (only for testing, see issue #369) + 5. Install to existing Ubuntu 16.04 server Enter the number of your desired provider : " @@ -379,8 +379,8 @@ Enter the number of your desired provider case "$N" in 1) digitalocean; ;; 2) ec2; ;; - 3) gce; ;; - 4) azure; ;; + 3) azure; ;; + 4) gce; ;; 5) non_cloud; ;; *) exit 1 ;; esac From 5e22b790333b7ed1a056c1cef1b5e6916ef0af79 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 09:52:23 -0400 Subject: [PATCH 409/769] Add configuration for URL probes to Apple profile Chrome and Android both request a known URL that generates HTTP 204 No Content responses to determine if they have internet connectivity. In Apple profiles, we can use the same URL to determine whether the VPN needs to connect. Using this feature will help save battery life for lots of users. --- roles/vpn/templates/mobileconfig.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index c48bc1b9..ec41fa91 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -37,6 +37,8 @@ {% endif %} InterfaceTypeMatch WiFi + URLStringProbe + http://www.gstatic.com/generate_204
Action @@ -47,6 +49,8 @@ {% endif %} InterfaceTypeMatch Cellular + URLStringProbe + http://www.gstatic.com/generate_204
{% else %} From 0256f3b31ca475985d2e3bcf48d67878955b829e Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:11:26 -0400 Subject: [PATCH 410/769] add virtualenv to CentOS instructions --- docs/pre-install_redhat_centos_6.x.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/pre-install_redhat_centos_6.x.md b/docs/pre-install_redhat_centos_6.x.md index ca598716..352d5490 100644 --- a/docs/pre-install_redhat_centos_6.x.md +++ b/docs/pre-install_redhat_centos_6.x.md @@ -40,15 +40,19 @@ source /opt/rh/python27/enable # Upgrade pip itself pip -q install --upgrade pip -# # python-devel needed to prevent setup.py crash, pycrypto 2.7.1 needed for latest security patch +# python-devel needed to prevent setup.py crash pip -q install pycrypto +# pycrypto 2.7.1 needed for latest security patch pip -q install setuptools --upgrade +# virtualenv to make installing dependencies easier +pip -q install virtualenv wget -q https://github.com/trailofbits/algo/archive/master.zip unzip master.zip cd algo-master || echo "No Algo directory found" -# Install the local Algo dependencies (must be run from algo-master) +# Set up a virtualenv and install the local Algo dependencies (must be run from algo-master) +virtualenv env && source env/bin/activate pip -q install -r requirements.txt # Edit the userlist and any other settings you desire From 7eb65ede2b2471f485cc985b3decb9d270643391 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:29:03 -0400 Subject: [PATCH 411/769] Update list of created artifacts --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3c3e2cac..3d538466 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,16 @@ You need to install the [strongSwan VPN Client for Android 4 and newer](https:// ### Windows -Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. -The PowerShell script has to be run as Administrator, so first open PowerShell as Administrator, then navigate to your copied files. If you have never used PowerShell before, you will need to change the Execution Policy to allow unsigned scripts to run. Execute the following command in PowerShell to do so. +Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. + +The PowerShell script has to be run as Administrator. Open PowerShell as Administrator, then navigate to your copied files. If you have never used PowerShell before, you will need to change the Execution Policy to allow unsigned scripts to run. Run the following command in PowerShell to do so. + ```powershell Set-ExecutionPolicy Unrestricted -Scope CurrentUser ``` -After you execute the user script remember to revert the policy change before you close the PowerShell window. + +After you execute the setup script, set this restriction back in place before you close the PowerShell window. + ```powershell Set-ExecutionPolicy Restricted -Scope CurrentUser ``` @@ -124,14 +128,13 @@ Install strongSwan, then copy the included user_ipsec.conf, user_ipsec.secrets, Depending on the platform, you may need one or multiple of the following files. -* ca.crt: CA Certificate -* user_ipsec.conf: strongSwan client configuration -* user_ipsec.secrets: strongSwan client configuration -* user.crt: User Certificate -* user.key: User Private Key +* cacert.pem: CA Certificate * user.mobileconfig: Apple Profile * user.p12: User Certificate and Private Key (in PKCS#12 format) -* user_windows.ps1: Powershell script to setup a VPN connection on Windows +* user.sswan: Android strongSwan Profile +* ipsec_user.conf: strongSwan client configuration +* ipsec_user.secrets: strongSwan client configuration +* windows_user.ps1: Powershell script to help setup a VPN connection on Windows ## Setup an SSH Tunnel From 2f122483469276ab93b5ee88e87f85e8f2448c46 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:31:59 -0400 Subject: [PATCH 412/769] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d538466..5c4f26e5 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,9 @@ Set-ExecutionPolicy Restricted -Scope CurrentUser If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: -`Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none` +```powershell +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none +``` ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu, etc.) @@ -177,6 +179,10 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. -- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) +> I played around with Algo VPN, a set of scripts that let you set up a VPN in the cloud in very little time, even if you don’t know much about development. I’ve got to say that I was quite impressed with Trail of Bits’ approach. + +-- Romain Dillet for [TechCrunch](https://techcrunch.com/2017/04/09/how-i-made-my-own-vpn-server-in-15-minutes/) + ## Support Algo VPN We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). All donations support continued development. From fe78ac0509bcd2423bda98ae7fda91524c91f574 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:32:53 -0400 Subject: [PATCH 413/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c4f26e5..1f626f20 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. > I played around with Algo VPN, a set of scripts that let you set up a VPN in the cloud in very little time, even if you don’t know much about development. I’ve got to say that I was quite impressed with Trail of Bits’ approach. --- Romain Dillet for [TechCrunch](https://techcrunch.com/2017/04/09/how-i-made-my-own-vpn-server-in-15-minutes/) +-- [Romain Dillet](https://twitter.com/romaindillet/status/851037243728965632) for [TechCrunch](https://techcrunch.com/2017/04/09/how-i-made-my-own-vpn-server-in-15-minutes/) ## Support Algo VPN From 6fdb735217382cf465b9271f33ce769324e95872 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:36:57 -0400 Subject: [PATCH 414/769] Update README.md --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f626f20..e3cbbfd4 100644 --- a/README.md +++ b/README.md @@ -185,8 +185,10 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Support Algo VPN -We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). All donations support continued development. +All donations support continued development. -Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. It helps support our development costs. +* We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). +* Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. +* We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. -We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. Thanks! +Thanks! From ea3775f90a413d5ae965ca2780d2d4dc796b6c60 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:37:16 -0400 Subject: [PATCH 415/769] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e3cbbfd4..325747ee 100644 --- a/README.md +++ b/README.md @@ -185,10 +185,8 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Support Algo VPN -All donations support continued development. +All donations support continued development. Thanks! * We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). * Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. * We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. - -Thanks! From e55ce03906b0bb34da6cecf249cef2c5337512e5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 10:44:32 -0400 Subject: [PATCH 416/769] URLStringProbe with this URL does not work as intended --- roles/vpn/templates/mobileconfig.j2 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index ec41fa91..c48bc1b9 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -37,8 +37,6 @@ {% endif %} InterfaceTypeMatch WiFi - URLStringProbe - http://www.gstatic.com/generate_204 Action @@ -49,8 +47,6 @@ {% endif %} InterfaceTypeMatch Cellular - URLStringProbe - http://www.gstatic.com/generate_204 {% else %} From 95e0134f2132ba08950327afe70de1db3d71fcc6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 9 Apr 2017 20:39:22 +0200 Subject: [PATCH 417/769] 1. Disable SSH key deploying if installation on existing server 2. Move to the ed25519 algorithm 3. Delete unneeded option RSAAuthentication Fixes #272 --- playbooks/common.yml | 2 +- playbooks/local.yml | 5 ++--- roles/security/templates/sshd_config.j2 | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/playbooks/common.yml b/playbooks/common.yml index 3dce6384..3308fa7b 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -17,4 +17,4 @@ user: "{{ ansible_ssh_user }}" state: present key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - tags: [ 'always' ] + tags: [ 'cloud' ] diff --git a/playbooks/local.yml b/playbooks/local.yml index bea14708..5376b647 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -1,14 +1,13 @@ --- - name: Generate the SSH private key - local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" + local_action: shell echo -e 'n' | ssh-keygen -C {{ SSH_keys.comment }} -t ed25519 -f {{ SSH_keys.private }} -q -N "" args: creates: "{{ SSH_keys.private }}" - name: Generate the SSH public key local_action: shell echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} > {{ SSH_keys.public }} - args: - creates: "{{ SSH_keys.public }}" + changed_when: false - name: Change mode for the SSH private key local_action: file path={{ SSH_keys.private }} mode=0600 diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index ebc93eeb..984f45c2 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -26,7 +26,6 @@ AcceptEnv LANG LC_* # Turn off a lot of features IgnoreRhosts yes RhostsRSAAuthentication no -RSAAuthentication no HostbasedAuthentication no PermitEmptyPasswords no ChallengeResponseAuthentication no @@ -53,4 +52,3 @@ MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@op # HostKeyAlgorithms ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 # TODO: I haven't seen anyone review these yet # PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 - From 70738ed8be44b7638430fbf3ae6b3eefde0d3c77 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 9 Apr 2017 20:52:54 +0200 Subject: [PATCH 418/769] Enable IP forwarding GCE #369 --- roles/cloud-gce/tasks/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 55e280a1..8df08cca 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -19,6 +19,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' + ip_forward: true tags: - "environment-algo" register: google_vm From ba0afbbf4aec9840415934b486d58c5f1f39c7a5 Mon Sep 17 00:00:00 2001 From: S Tung Date: Sun, 9 Apr 2017 17:36:35 -0400 Subject: [PATCH 419/769] Update windows steps for clarity (#377) --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 325747ee..7dc883ce 100644 --- a/README.md +++ b/README.md @@ -102,21 +102,23 @@ You need to install the [strongSwan VPN Client for Android 4 and newer](https:// ### Windows -Copy the CA certificate, user certificate, and the user PowerShell script to the client computer. Import the CA certificate to the local machine Trusted Root certificate store. Then, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. - -The PowerShell script has to be run as Administrator. Open PowerShell as Administrator, then navigate to your copied files. If you have never used PowerShell before, you will need to change the Execution Policy to allow unsigned scripts to run. Run the following command in PowerShell to do so. +1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. +2. Import the CA certificate to the local machine Trusted Root certificate store. +3. Open PowerShell as Administrator. Navigate to your copied files. +4. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. ```powershell Set-ExecutionPolicy Unrestricted -Scope CurrentUser ``` -After you execute the setup script, set this restriction back in place before you close the PowerShell window. +5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. +6. After you execute the user script remember to revert the policy change before you close the PowerShell window. ```powershell Set-ExecutionPolicy Restricted -Scope CurrentUser ``` -If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: +And that's it! If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: ```powershell Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none From 816a16e8842e55753916119cb17849312fb89fb5 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 9 Apr 2017 17:42:51 -0400 Subject: [PATCH 420/769] Windows is so ugly --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7dc883ce..2677675d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ You need to install the [strongSwan VPN Client for Android 4 and newer](https:// ### Windows +Windows clients have a more complicated setup than most others. Follow the steps below to set one up: + 1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. 2. Import the CA certificate to the local machine Trusted Root certificate store. 3. Open PowerShell as Administrator. Navigate to your copied files. @@ -111,22 +113,24 @@ You need to install the [strongSwan VPN Client for Android 4 and newer](https:// Set-ExecutionPolicy Unrestricted -Scope CurrentUser ``` -5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. -6. After you execute the user script remember to revert the policy change before you close the PowerShell window. +5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. +6. After you execute the user script, set the Execution Policy back before you close the PowerShell window. ```powershell Set-ExecutionPolicy Restricted -Scope CurrentUser ``` -And that's it! If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: +Your VPN is now installed and ready to use. + +If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: ```powershell Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none ``` -### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu, etc.) +### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu Server, etc.) -Install strongSwan, then copy the included user_ipsec.conf, user_ipsec.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These may require some customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. +Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. ### Other Devices From 2ec6f41e0ff8fb304e300a872a6f5ffed92e236d Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 10 Apr 2017 06:47:10 +0200 Subject: [PATCH 421/769] decrease mss --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index 525064f0..45466104 100755 --- a/algo +++ b/algo @@ -329,7 +329,7 @@ Please choose the number of your zone. Press enter for default (#8) zone. esac ROLES="gce vpn cloud" - EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1348" + EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1316" } non_cloud () { From 25e0e9085d879d034518d0bf09c59124d55fbf00 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 10 Apr 2017 07:22:40 +0200 Subject: [PATCH 422/769] move back to RSA --- playbooks/local.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/local.yml b/playbooks/local.yml index 5376b647..e852bc20 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -1,7 +1,7 @@ --- - name: Generate the SSH private key - local_action: shell echo -e 'n' | ssh-keygen -C {{ SSH_keys.comment }} -t ed25519 -f {{ SSH_keys.private }} -q -N "" + local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" args: creates: "{{ SSH_keys.private }}" From 6e1b0df70085af4aa33481c165b8707ada4b4ddf Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 11 Apr 2017 13:56:25 -0400 Subject: [PATCH 423/769] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8fd2698a..3c32a91d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ * Check that your issue is not already described in the [FAQ](docs/FAQ.md), [troubleshooting](docs/TROUBLESHOOTING.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) * Did you remember to install the dependencies for your operating system prior to installing Algo? -* Client support is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 8+, Ubuntu 17.04+, etc. +* We only support modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 10+, Ubuntu 17.04+, etc. * Cloud provider support is limited to DO, AWS, GCE, and Azure. Any others are best effort only. * If you need to file a new issue, fill out any relevant fields in the Issue Template From 56a72e5af20124959ad4145c7db88ef1891d5f9e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 11 Apr 2017 22:08:03 +0200 Subject: [PATCH 424/769] New ciphers implementing #247 (#352) Switches to SHA2_512_256 HMAC integrity algorithm and adds cipher compatibility for other platforms. --- roles/cloud-gce/tasks/main.yml | 2 +- roles/vpn/defaults/main.yml | 8 ++++++++ roles/vpn/templates/client_ipsec.conf.j2 | 8 ++++---- roles/vpn/templates/client_windows.ps1.j2 | 2 +- roles/vpn/templates/ipsec.conf.j2 | 8 ++++---- roles/vpn/templates/mobileconfig.j2 | 4 ++-- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 8df08cca..5c6a1f66 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -19,7 +19,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - ip_forward: true + # ip_forward: true tags: - "environment-algo" register: google_vm diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index cc3ee72a..db312818 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -19,3 +19,11 @@ strongswan_enabled_plugins: - socket-default - stroke - x509 + +ciphers: + defaults: + ike: aes128gcm16-sha2_512-prfsha512-ecp256! + esp: aes128gcm16-sha2_512-ecp256! + compat: + ike: aes128-sha2_512-prfsha512-ecp256,aes128gcm16-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! + esp: aes128-sha2_512-ecp256,aes128gcm16-sha2_512-ecp256,aes128-sha2_256-modp2048! diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index ffdbcc89..8a12d7de 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -7,11 +7,11 @@ conn ikev2-{{ IP_subject_alt_name }} dpddelay=35s {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} - ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! - esp=aes128gcm16-sha2_256-ecp256,aes256-sha1-modp1024! + ike={{ ciphers.compat.ike }} + esp={{ ciphers.compat.esp }} {% else %} - ike=aes128gcm16-sha2_256-prfsha256-ecp256 - esp=aes128gcm16-sha2_256-ecp256 + ike={{ ciphers.defaults.ike }} + esp={{ ciphers.defaults.esp }} {% endif %} right={{ IP_subject_alt_name }} diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index aa5b708b..4df2297f 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,3 @@ certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 1b3aa7f5..03211b94 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -11,11 +11,11 @@ conn %default dpddelay=35s {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} - ike=aes128gcm16-sha2_256-prfsha256-ecp256,aes256-sha2_256-prfsha256-modp2048! - esp=aes128gcm16-sha2_256-ecp256,aes256-sha2_256-modp2048! + ike={{ ciphers.compat.ike }} + esp={{ ciphers.compat.esp }} {% else %} - ike=aes128gcm16-sha2_256-prfsha256-ecp256! - esp=aes128gcm16-sha2_256-ecp256! + ike={{ ciphers.defaults.ike }} + esp={{ ciphers.defaults.esp }} {% endif %} left=%any diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index c48bc1b9..e9548452 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -60,7 +60,7 @@ EncryptionAlgorithm AES-128-GCM IntegrityAlgorithm - SHA2-256 + SHA2-512 LifeTimeInMinutes 20 @@ -81,7 +81,7 @@ EncryptionAlgorithm AES-128-GCM IntegrityAlgorithm - SHA2-256 + SHA2-512 LifeTimeInMinutes 20 From 094c8eede1d3a403991cf174bf00b003d4324886 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 12 Apr 2017 20:14:27 +0200 Subject: [PATCH 425/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 83dd269f..a7c92bf1 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,10 +7,10 @@ ### Version of components from `requirements.txt` -``` + ### Summary of the problem From de12d51859b05062ed1b4159017f939d01da2fc2 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 12 Apr 2017 20:25:31 -0400 Subject: [PATCH 426/769] Reorganize documentation for GitHub pages (#378) Reorganize documentation for clarity and use with GitHub pages static site generator. This closes #371. --- CONTRIBUTING.md | 2 +- README.md | 18 ++++---- algo | 8 ++-- docs/{ADVANCED.md => advanced-usage.md} | 10 ++--- docs/{ROLES.md => ansible-roles.md} | 4 +- docs/{ANDROID.md => client-android.md} | 4 +- docs/{CLIENT.md => client-linux.md} | 6 ++- docs/{WINDOWS.md => client-windows.md} | 12 ++--- docs/{AZURE.md => cloud-azure.md} | 4 +- docs/{FAQ.md => faq.md} | 30 ++++++------- docs/index.md | 15 +++++++ docs/{FreeBSD.md => server-freebsd.md} | 10 +++-- ...centos_6.x.md => server-redhat-centos6.md} | 45 ++++++++++++------- ...{TROUBLESHOOTING.md => troubleshooting.md} | 2 + 14 files changed, 105 insertions(+), 65 deletions(-) rename docs/{ADVANCED.md => advanced-usage.md} (97%) rename docs/{ROLES.md => ansible-roles.md} (98%) rename docs/{ANDROID.md => client-android.md} (97%) rename docs/{CLIENT.md => client-linux.md} (79%) rename docs/{WINDOWS.md => client-windows.md} (85%) rename docs/{AZURE.md => cloud-azure.md} (97%) rename docs/{FAQ.md => faq.md} (76%) create mode 100644 docs/index.md rename docs/{FreeBSD.md => server-freebsd.md} (60%) rename docs/{pre-install_redhat_centos_6.x.md => server-redhat-centos6.md} (74%) rename docs/{TROUBLESHOOTING.md => troubleshooting.md} (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c32a91d..250dbbfa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ ### Filing New Issues -* Check that your issue is not already described in the [FAQ](docs/FAQ.md), [troubleshooting](docs/TROUBLESHOOTING.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) +* Check that your issue is not already described in the [FAQ](docs/faq.md), [troubleshooting](docs/troubleshooting.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) * Did you remember to install the dependencies for your operating system prior to installing Algo? * We only support modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 10+, Ubuntu 17.04+, etc. * Cloud provider support is limited to DO, AWS, GCE, and Azure. Any others are best effort only. diff --git a/README.md b/README.md index 2677675d..550a22e2 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua python-setuptools \ python-virtualenv -y ``` - - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/pre-install_redhat_centos_6.x.md) - - Windows: See the [Windows documentation](docs/WINDOWS.md) + - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/server-redhat-centos-6.md) + - Windows: See the [Windows documentation](docs/client-windows.md) 4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. ```bash @@ -66,7 +66,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ROLES.md](docs/ROLES.md). +6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ansible-roles.md](docs/ansible-roles.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. @@ -86,7 +86,7 @@ You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to Note: If you want to run Algo again at any point in the future, you must first "reactivate" the dependencies for it. To reactivate them, open your terminal, use `cd` to navigate to the directory with Algo, then run `source env/bin/activate`. -Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/ADVANCED.md) documentation. +Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/advanced-usage.md) documentation. ## Configure the VPN Clients @@ -98,7 +98,7 @@ Find the corresponding mobileconfig (Apple Profile) for each user and send it to ### Android Devices -You need to install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/ANDROID.md) for more detailed steps. +You need to install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more detailed steps. ### Windows @@ -164,10 +164,10 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation -* [Advanced Usage](docs/ADVANCED.md) describes how to deploy an Algo VPN server directly from Ansible. -* [FAQ](docs/FAQ.md) includes answers to common questions. -* [Roles](docs/ROLES.md) includes a description of optional Algo VPN server features. -* [Troubleshooting](docs/TROUBLESHOOTING.md) includes answers to common technical issues. +* [Advanced Usage](docs/anvanced-usage.md) describes how to deploy an Algo VPN server directly from Ansible. +* [FAQ](docs/faq.md) includes answers to common questions. +* [Roles](docs/ansible-roles.md) includes a description of optional Algo VPN server features. +* [Troubleshooting](docs/troubleshooting.md) includes answers to common technical issues. ## Endorsements diff --git a/algo b/algo index 45466104..4b088f7e 100755 --- a/algo +++ b/algo @@ -67,28 +67,28 @@ deploy () { azure () { read -p " -Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) +Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials $ADDITIONAL_PROMPT [...]: " -rs azure_secret read -p " -Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) +Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials $ADDITIONAL_PROMPT [...]: " -rs azure_tenant read -p " -Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) +Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials $ADDITIONAL_PROMPT [...]: " -rs azure_client_id read -p " -Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/AZURE.md) +Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) You can skip this step if you want to use your defaults credentials from ~/.azure/credentials $ADDITIONAL_PROMPT [...]: " -rs azure_subscription_id diff --git a/docs/ADVANCED.md b/docs/advanced-usage.md similarity index 97% rename from docs/ADVANCED.md rename to docs/advanced-usage.md index 8a7508f0..5f1b18a9 100644 --- a/docs/ADVANCED.md +++ b/docs/advanced-usage.md @@ -2,13 +2,13 @@ Make sure you have installed all the dependencies necessary for your operating system as described in the [README](../README.md). -## Local Deployment +## Local deployment It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install Ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. tl;dr: -``` +```shell sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible sudo apt-get update && sudo apt-get install ansible git clone https://github.com/trailofbits/algo @@ -17,7 +17,7 @@ cd algo && ./algo **Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. -## Scripted Deployment +## Scripted deployment You can deploy Algo non-interactively by running the Ansible playbooks directly with `ansible-playbook`. @@ -27,11 +27,11 @@ You can deploy Algo non-interactively by running the Ansible playbooks directly Here is a full example for DigitalOcean: -``` +```shell ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' ``` -### Roles +### Ansible roles Required tags: diff --git a/docs/ROLES.md b/docs/ansible-roles.md similarity index 98% rename from docs/ROLES.md rename to docs/ansible-roles.md index 5c90007c..2e224c13 100644 --- a/docs/ROLES.md +++ b/docs/ansible-roles.md @@ -1,6 +1,6 @@ # Ansible Roles -## Required Roles +## Required roles * **Common** * Installs several required packages and software updates, then reboots if necessary @@ -11,7 +11,7 @@ * Bundles the appropriate certificates into Apple mobileconfig profiles for each user * Configures IPtables to block traffic that might pose a risk to VPN users, such as [SMB/CIFS](https://medium.com/@ValdikSS/deanonymizing-windows-users-and-capturing-microsoft-and-vpn-accounts-f7e53fe73834) -## Optional Roles +## Optional roles * **Security Enhancements** * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied diff --git a/docs/ANDROID.md b/docs/client-android.md similarity index 97% rename from docs/ANDROID.md rename to docs/client-android.md index 1f6faba0..bd71dddb 100644 --- a/docs/ANDROID.md +++ b/docs/client-android.md @@ -1,3 +1,5 @@ +# Android client setup + **NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [strongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. | Instruction | Screenshot(s) | @@ -35,4 +37,4 @@ Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before at [step7-screen]: https://i.imgur.com/aT2MPih.png [step8-screen]: https://i.imgur.com/gvaKzkh.png [step9-screen]: https://i.imgur.com/eZp8DNb.png -[step10-screen]: https://i.imgur.com/Nd8rYMJ.png \ No newline at end of file +[step10-screen]: https://i.imgur.com/Nd8rYMJ.png diff --git a/docs/CLIENT.md b/docs/client-linux.md similarity index 79% rename from docs/CLIENT.md rename to docs/client-linux.md index 41d0f96d..aabd5f3e 100644 --- a/docs/CLIENT.md +++ b/docs/client-linux.md @@ -1,4 +1,4 @@ -# Client installation +# Linux client setup It's possible to deploy an ipsec connection on Linux clients. Supported distributives are: Debian, Ubuntu, CentOS, Fedora @@ -14,4 +14,6 @@ The playbook is `deploy_client.yml` ### Example: -`ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root'` +```shell +ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root' +``` diff --git a/docs/WINDOWS.md b/docs/client-windows.md similarity index 85% rename from docs/WINDOWS.md rename to docs/client-windows.md index fcd5d977..e261078e 100644 --- a/docs/WINDOWS.md +++ b/docs/client-windows.md @@ -1,6 +1,4 @@ -# Windows - -## How to run Algo on Windows 10 +# Windows client prerequisites Before run Algo, you have to have: @@ -23,10 +21,14 @@ The subsystem will be installed, then Windows will require a reboot. Reboot, the Install additional packages: -`sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv git -y` +```shell +sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv git -y +``` Clone the Algo repository: -`git clone https://github.com/trailofbits/algo && cd algo` +```shell +git clone https://github.com/trailofbits/algo && cd algo +``` Now, you can go through the [README](https://github.com/trailofbits/algo#deploy-the-algo-server) (start from the 4th step) and deploy your Algo server! diff --git a/docs/AZURE.md b/docs/cloud-azure.md similarity index 97% rename from docs/AZURE.md rename to docs/cloud-azure.md index 909d12e4..cb9c20c1 100644 --- a/docs/AZURE.md +++ b/docs/cloud-azure.md @@ -1,4 +1,4 @@ -### Authenticating with Azure +# Azure cloud setup | Instruction | Screenshot(s) | | ----------- | ---------- | @@ -22,7 +22,7 @@ Now you can use Environment Variables: * AZURE_TENANT - from the 8th step * AZURE_SUBSCRIPTION_ID - from the 9th step -or create the credentials file ~/.azure/credentials: +or create the credentials file ``~/.azure/credentials`: ``` [default] diff --git a/docs/FAQ.md b/docs/faq.md similarity index 76% rename from docs/FAQ.md rename to docs/faq.md index e18f8523..29fe17c4 100644 --- a/docs/FAQ.md +++ b/docs/faq.md @@ -1,37 +1,37 @@ -## FAQ +# FAQ -1. [Has Algo been audited?](#1-has-algo-been-audited) -2. [Why aren't you using Tor?](#2-why-arent-you-using-tor) -3. [Why aren't you using Racoon, LibreSwan, or OpenSwan?](#3-why-arent-you-using-racoon-libreswan-or-openswan) -4. [Why aren't you using a memory-safe or verified IKE daemon?](#4-why-arent-you-using-a-memory-safe-or-verified-ike-daemon) -5. [Why aren't you using OpenVPN?](#5-why-arent-you-using-openvpn) -6. [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#6-why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) -7. [Where did the name "Algo" come from?](#7-where-did-the-name-algo-come-from) +[Has Algo been audited?](#has-algo-been-audited) +[Why aren't you using Tor?](#why-arent-you-using-tor) +[Why aren't you using Racoon, LibreSwan, or OpenSwan?](#why-arent-you-using-racoon-libreswan-or-openswan) +[Why aren't you using a memory-safe or verified IKE daemon?](#why-arent-you-using-a-memory-safe-or-verified-ike-daemon) +[Why aren't you using OpenVPN?](#why-arent-you-using-openvpn) +[Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) +[Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) -### 1. Has Algo been audited? +## Has Algo been audited? No. This project is under [active development](https://github.com/trailofbits/algo/projects/1). We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). -### 2. Why aren't you using Tor? +## Why aren't you using Tor? The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). -### 3. Why aren't you using Racoon, LibreSwan, or OpenSwan? +## Why aren't you using Racoon, LibreSwan, or OpenSwan? Racoon does not support IKEv2. Racoon2 supports IKEv2 but is not actively maintained. When we looked, the documentation for strongSwan was better than the corresponding documentation for LibreSwan or OpenSwan. strongSwan also has the benefit of a from-scratch rewrite to support IKEv2. I consider such rewrites a positive step when supporting a major new protocol version. -### 4. Why aren't you using a memory-safe or verified IKE daemon? +## Why aren't you using a memory-safe or verified IKE daemon? I would, but I don't know of any [suitable ones](https://github.com/trailofbits/algo/issues/68). If you're in the position to fund the development of such a project, [contact us](mailto:info@trailofbits.com). We would be interested in leading such an effort. At the very least, I plan to make modifications to strongSwan and the environment it's deployed in that prevent or significantly complicate exploitation of any latent issues. -### 5. Why aren't you using OpenVPN? +## Why aren't you using OpenVPN? OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). -### 6. Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? +## Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). -### 7. Where did the name "Algo" come from? +## Where did the name "Algo" come from? Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..2cbded82 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,15 @@ +# Algo VPN documentation + +* [Advanced usage](advanced-usage.md) +* [Ansible roles](ansible-roles.md) +* Client setup + - [Windows](client-windows.md) + - [Android](client-android.md) + - [Generic/Linux](client-generic.md) +* Cloud setup + - [Azure](cloud-azure.md) +* Server setup + - [RedHat/CentOS 6.x](server-centos6.md) + - [FreeBSD](server-freebsd.md) +* [Troubleshooting](troubleshooting.md) +* [FAQ](faq.md) diff --git a/docs/FreeBSD.md b/docs/server-freebsd.md similarity index 60% rename from docs/FreeBSD.md rename to docs/server-freebsd.md index f1a8c838..f78f4b0e 100644 --- a/docs/FreeBSD.md +++ b/docs/server-freebsd.md @@ -1,8 +1,8 @@ -# FreeBSD / HardenedBSD +# FreeBSD / HardenedBSD server setup It is only possible to install Algo on existing systems only. We support only 11 version for now. -## Pre-paring the system +## System preparation Ensure that the following kernel options are enabled: @@ -21,8 +21,10 @@ device crypto ## Additional variables -* rebuild_kernel - set to `true` if you want to let Algo to rebuild your kernel if needed (Takes a lot of time) +* rebuild_kernel - set to `true` if you want to let Algo to rebuild your kernel if needed (takes a lot of time) ## Installation -`ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$server_ip Store_CAKEY=N" --skip-tags cloud` +```shell +ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$server_ip Store_CAKEY=N" --skip-tags cloud +``` diff --git a/docs/pre-install_redhat_centos_6.x.md b/docs/server-redhat-centos6.md similarity index 74% rename from docs/pre-install_redhat_centos_6.x.md rename to docs/server-redhat-centos6.md index 352d5490..5ed9cc66 100644 --- a/docs/pre-install_redhat_centos_6.x.md +++ b/docs/server-redhat-centos6.md @@ -1,42 +1,48 @@ -# Algo pre-install steps for Red Hat/CentOS 6.x (currently 6.8) +# RedHat/CentOS 6.x pre-installation requirements Many people prefer RedHat or CentOS 6 (or similar variants like Amazon Linux) for to their stability and lack of systemd. Unfortunately, there are a number of dated libraries, notably Python 2.6, that prevent Algo from running without errors. This script will prepare a RedHat, CentOS, or similar VM to deploy to Algo cloud instances. ## Step 1: Prep for RH/CentOS 6.8/Amazon -``` +```shell yum -y -q update yum -y -q install epel-release ``` Enable any kernel updates: -``reboot`` +```shell +reboot +``` -## Step 2: Install Ansible & launch Algo +## Step 2: Install Ansible and launch Algo Fix GPG key warnings during Ansible rpm install: -``rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6`` +```shell +rpm --import https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6 +``` Fix GPG key warning during official Software Collections (SCL) package install: -``rpm --import https://raw.githubusercontent.com/sclorg/centos-release-scl/master/centos-release-scl/RPM-GPG-KEY-CentOS-SIG-SCLo`` +```shell +rpm --import https://raw.githubusercontent.com/sclorg/centos-release-scl/master/centos-release-scl/RPM-GPG-KEY-CentOS-SIG-SCLo +``` RedHat/CentOS 6.x uses Python 2.6 by default, which is explicitly deprecated and produces many warnings and errors, so we must install a safe, non-invasive 2.7 tool set which has to be expressly enabled (and will not survive login sessions and reboots): -``` +```shell # Install the Software Collections Library (to enable Python 2.7) yum -y -q install centos-release-SCL # 2.7 will not be used until explicitly enabled, per login session yum -y -q install python27-python-devel python27-python-setuptools python27-python-pip -yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel wget unzip ansible nano +yum -y -q install openssl-devel libffi-devel automake gcc gcc-c++ kernel-devel wget unzip ansible nano # Enable 2.7 default for this session (needs re-run between logins & reboots) # shellcheck disable=SC1091 source /opt/rh/python27/enable -# We're now defaulted to 2.7 +# We're now defaulted to 2.7 # Upgrade pip itself pip -q install --upgrade pip @@ -48,7 +54,7 @@ pip -q install setuptools --upgrade pip -q install virtualenv wget -q https://github.com/trailofbits/algo/archive/master.zip -unzip master.zip +unzip master.zip cd algo-master || echo "No Algo directory found" # Set up a virtualenv and install the local Algo dependencies (must be run from algo-master) @@ -61,11 +67,20 @@ nano config.cfg ./algo ``` -## Post-install OSX +## Post-install macOS -* Copy ./configs/*mobileconfig to your local Mac -* Install the VPN profile on your Mac (10.10+ required) - * ``/usr/bin/profiles -I -F ./x.x.x.x_NAME.mobileconfig`` -* To remove: ```/usr/bin/profiles -D -F ./x.x.x.x_NAME.mobileconfig``` +1. Copy `./configs/*mobileconfig` to your local Mac + +2. Install the VPN profile on your Mac (10.10+ required) + + ```shell + /usr/bin/profiles -I -F ./x.x.x.x_NAME.mobileconfig + ``` + +3. To remove: + + ```shell + /usr/bin/profiles -D -F ./x.x.x.x_NAME.mobileconfig + ``` The VPN connection will now appear under Networks (which can be pinned to the top menu bar if preferred) diff --git a/docs/TROUBLESHOOTING.md b/docs/troubleshooting.md similarity index 99% rename from docs/TROUBLESHOOTING.md rename to docs/troubleshooting.md index 8c4c0358..118b8d20 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/troubleshooting.md @@ -1,3 +1,5 @@ +# Troubleshooting + ## Table of Contents 1. [Error: "You have not agreed to the Xcode license agreements"](#1-error-you-have-not-agreed-to-the-xcode-license-agreements) From a13b56a513ae7413323203841c193202743e15b0 Mon Sep 17 00:00:00 2001 From: mathew19 Date: Thu, 13 Apr 2017 11:11:18 -0400 Subject: [PATCH 427/769] Update README.md (#395) Added Ubuntu 16.04 example --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 550a22e2..d35d1067 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,15 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransf Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. +#### Ubuntu 16.04 example +1. Edit ```/etc/ipsec.conf``` to add the connection from ipsec_user.conf. Make sure that the ```leftcert``` entry is correct for the user.crt filename. +2. Edit ```/etc/ipsec.secrets``` to add your user.key entry. Again make sure your file name is correct. + e.g. ```xx.xxx.xx.xxx : ECDSA user.key``` +3. Copy ```user.crt``` to ```/etc/ipsec.d/certs``` +4. Copy ```user.key``` to ```/etc/ipsec.d/private``` +5. Start the client connection with ```sudo ipsec up ``` +6. Shutdown if needed with ```sudo ipsec down ``` + ### Other Devices Depending on the platform, you may need one or multiple of the following files. From 5b6386ca08bbc6edabc0e9736f95f4f3809b08d6 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Apr 2017 11:59:49 -0400 Subject: [PATCH 428/769] readability --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d35d1067..05dd6787 100644 --- a/README.md +++ b/README.md @@ -132,14 +132,14 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransf Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. -#### Ubuntu 16.04 example -1. Edit ```/etc/ipsec.conf``` to add the connection from ipsec_user.conf. Make sure that the ```leftcert``` entry is correct for the user.crt filename. -2. Edit ```/etc/ipsec.secrets``` to add your user.key entry. Again make sure your file name is correct. - e.g. ```xx.xxx.xx.xxx : ECDSA user.key``` -3. Copy ```user.crt``` to ```/etc/ipsec.d/certs``` -4. Copy ```user.key``` to ```/etc/ipsec.d/private``` -5. Start the client connection with ```sudo ipsec up ``` -6. Shutdown if needed with ```sudo ipsec down ``` +#### Ubuntu Server 16.04 example + +1. `/etc/ipsec.d/certs`: copy `user.crt` here +2. `/etc/ipsec.d/private`: copy `user.key` here +3. `/etc/ipsec.secrets`: add your `user.key`, e.g. `xx.xxx.xx.xxx : ECDSA user.key`. +4. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update the value for `leftcert` +5. `sudo ipsec up `: start the ipsec tunnel +6. `sudo ipsec down `: shutdown the ipsec tunnel ### Other Devices From 5521e803bbd207e81ac6b14c3dddb96b36a82a8b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Apr 2017 12:00:46 -0400 Subject: [PATCH 429/769] style --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05dd6787..9c58a4a2 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, 1. `/etc/ipsec.d/certs`: copy `user.crt` here 2. `/etc/ipsec.d/private`: copy `user.key` here -3. `/etc/ipsec.secrets`: add your `user.key`, e.g. `xx.xxx.xx.xxx : ECDSA user.key`. +3. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key` 4. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update the value for `leftcert` 5. `sudo ipsec up `: start the ipsec tunnel 6. `sudo ipsec down `: shutdown the ipsec tunnel From 85ca2652607499051185a54211d2588035a7cd42 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Apr 2017 15:34:44 -0400 Subject: [PATCH 430/769] Update faq.md --- docs/faq.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 29fe17c4..8ff4a3be 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,12 +1,12 @@ # FAQ -[Has Algo been audited?](#has-algo-been-audited) -[Why aren't you using Tor?](#why-arent-you-using-tor) -[Why aren't you using Racoon, LibreSwan, or OpenSwan?](#why-arent-you-using-racoon-libreswan-or-openswan) -[Why aren't you using a memory-safe or verified IKE daemon?](#why-arent-you-using-a-memory-safe-or-verified-ike-daemon) -[Why aren't you using OpenVPN?](#why-arent-you-using-openvpn) -[Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) -[Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) +* [Has Algo been audited?](#has-algo-been-audited) +* [Why aren't you using Tor?](#why-arent-you-using-tor) +* [Why aren't you using Racoon, LibreSwan, or OpenSwan?](#why-arent-you-using-racoon-libreswan-or-openswan) +* [Why aren't you using a memory-safe or verified IKE daemon?](#why-arent-you-using-a-memory-safe-or-verified-ike-daemon) +* [Why aren't you using OpenVPN?](#why-arent-you-using-openvpn) +* [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) +* [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) ## Has Algo been audited? From a5ea5d8bb5ffba78293224bd1f3e4af3fc92d378 Mon Sep 17 00:00:00 2001 From: Rob Nee Date: Thu, 13 Apr 2017 19:09:28 -0400 Subject: [PATCH 431/769] Fix link typo (#397) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c58a4a2..9e5f0c36 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua python-setuptools \ python-virtualenv -y ``` - - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/server-redhat-centos-6.md) + - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/server-redhat-centos6.md) - Windows: See the [Windows documentation](docs/client-windows.md) 4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. From 41fae80f69b08ebbef14f3cb80caf9b64c37ba26 Mon Sep 17 00:00:00 2001 From: Rob Nee Date: Thu, 13 Apr 2017 19:39:53 -0400 Subject: [PATCH 432/769] Fix link to RedHat/Centos doc (#398) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 2cbded82..24dd4aac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ * Cloud setup - [Azure](cloud-azure.md) * Server setup - - [RedHat/CentOS 6.x](server-centos6.md) + - [RedHat/CentOS 6.x](server-redhat-centos6.md) - [FreeBSD](server-freebsd.md) * [Troubleshooting](troubleshooting.md) * [FAQ](faq.md) From 09cde1c28ff495c98cdddc45ee20ff66eaa877dc Mon Sep 17 00:00:00 2001 From: Theophile Trunck Date: Thu, 13 Apr 2017 21:52:09 -0400 Subject: [PATCH 433/769] fix Advanced Usage broken link (#399) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e5f0c36..7572d6df 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation -* [Advanced Usage](docs/anvanced-usage.md) describes how to deploy an Algo VPN server directly from Ansible. +* [Advanced Usage](docs/advanced-usage.md) describes how to deploy an Algo VPN server directly from Ansible. * [FAQ](docs/faq.md) includes answers to common questions. * [Roles](docs/ansible-roles.md) includes a description of optional Algo VPN server features. * [Troubleshooting](docs/troubleshooting.md) includes answers to common technical issues. From e7593ab8b8cece2054c64463b23d52a2199348f5 Mon Sep 17 00:00:00 2001 From: mathew19 Date: Thu, 13 Apr 2017 22:15:04 -0400 Subject: [PATCH 434/769] Update TROUBLESHOOTING.md (#393) * Update TROUBLESHOOTING.md Added example for problems with MTU size on Linux(Ubuntu) * Update troubleshooting.md * fix Advanced Usage broken link (#399) * Update TROUBLESHOOTING.md Added example for problems with MTU size on Linux(Ubuntu) --- docs/troubleshooting.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 118b8d20..34f9ac5d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -99,6 +99,17 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +e.g. In Linux (client -- Ubuntu 16.04), determine your MTU size (after you're connected via ipsec) +``` +$ ping -M do -s 1500 www.google.com +PING www.google.com (74.125.22.147) 1500(1528) bytes of data. +ping: local error: Message too long, mtu=1438 +``` +Set your MTU size on the network adapter (wlan0 or eth0) +``` +$ sudo ifconfig wlan0 mtu 1438 +``` + ### 8. The region you want is not available You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. From 75cc96f9d648187a25ac46aced1291aee9b8b112 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Apr 2017 22:16:50 -0400 Subject: [PATCH 435/769] Update troubleshooting.md --- docs/troubleshooting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 34f9ac5d..d1fc485c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -97,15 +97,15 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and ### 7. Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit size and decreasing packet size. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. -e.g. In Linux (client -- Ubuntu 16.04), determine your MTU size (after you're connected via ipsec) +E.g., On Linux (client -- Ubuntu 16.04), use the following commands to determine your MTU size after you're connected via the IPsec tunnel: ``` $ ping -M do -s 1500 www.google.com PING www.google.com (74.125.22.147) 1500(1528) bytes of data. ping: local error: Message too long, mtu=1438 ``` -Set your MTU size on the network adapter (wlan0 or eth0) +Then, set the MTU size on your network adapter (wlan0 or eth0): ``` $ sudo ifconfig wlan0 mtu 1438 ``` From 77ad0576bf3e408af4192b567d7a4c8df5342a87 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 13 Apr 2017 22:20:36 -0400 Subject: [PATCH 436/769] Update troubleshooting.md --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d1fc485c..273a0ffe 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -99,7 +99,7 @@ Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. -E.g., On Linux (client -- Ubuntu 16.04), use the following commands to determine your MTU size after you're connected via the IPsec tunnel: +E.g., On Linux (client -- Ubuntu 16.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: ``` $ ping -M do -s 1500 www.google.com PING www.google.com (74.125.22.147) 1500(1528) bytes of data. From c61a07fb601896ed7e06534a5a642227121993be Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 14 Apr 2017 20:57:27 +0200 Subject: [PATCH 437/769] Escaping Special Characters #388 (#403) --- roles/vpn/templates/mobileconfig.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index e9548452..b22bfa86 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -22,7 +22,7 @@ SSIDMatch {% for network_name in WIFI_EXCLUDE_LIST %} - {{ network_name }} + {{ network_name|e }} {% endfor %} From 5e56996f5ca5e741f8e4896308675da591aced5e Mon Sep 17 00:00:00 2001 From: mathew19 Date: Sat, 15 Apr 2017 08:57:07 -0400 Subject: [PATCH 438/769] Fix name (#411) --- roles/vpn/templates/client_ipsec.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 8a12d7de..7fde04ab 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -21,7 +21,7 @@ conn ikev2-{{ IP_subject_alt_name }} leftsourceip=%config leftauth=pubkey - leftcert={{ IP_subject_alt_name }}_{{ item }}.crt + leftcert={{ item }}.crt leftfirewall=yes left=%defaultroute From ae43ed6f81abeecd1d9ff1fcb8ffa34fe2fac9f0 Mon Sep 17 00:00:00 2001 From: mathew19 Date: Sat, 15 Apr 2017 08:57:22 -0400 Subject: [PATCH 439/769] Update client_ipsec.secrets.j2 (#414) Fix filename in client ipsec_user.secrets --- roles/vpn/templates/client_ipsec.secrets.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/vpn/templates/client_ipsec.secrets.j2 index 61603129..0d8356ee 100644 --- a/roles/vpn/templates/client_ipsec.secrets.j2 +++ b/roles/vpn/templates/client_ipsec.secrets.j2 @@ -1,5 +1,5 @@ {% if Win10_Enabled is defined and Win10_Enabled == "Y" %} -{{ IP_subject_alt_name }} : RSA {{ IP_subject_alt_name }}_{{ item }}.key +{{ IP_subject_alt_name }} : RSA {{ item }}.key {% else %} -{{ IP_subject_alt_name }} : ECDSA {{ IP_subject_alt_name }}_{{ item }}.key +{{ IP_subject_alt_name }} : ECDSA {{ item }}.key {% endif %} From 57b9cf3db1706301dda3bd84d29236c9317e7072 Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Sat, 15 Apr 2017 06:01:07 -0700 Subject: [PATCH 440/769] adding sa-east-1 region and auto sourcing env/bin/activate (#402) --- algo | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 4b088f7e..a9d4914c 100755 --- a/algo +++ b/algo @@ -2,6 +2,15 @@ set -e +ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" +if [ -f "$ACTIVATE_SCRIPT" ] +then + source $ACTIVATE_SCRIPT +else + echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" + exit 1 +fi + SKIP_TAGS="_null encrypted" ADDITIONAL_PROMPT="[pasted values will not be displayed]" @@ -252,10 +261,10 @@ Name the vpn server: 11. eu-west-1 EU (Ireland) 12. eu-west-2 EU (London) 13. ca-central-1 Canada (Central) + 14. sa-east-1 São Paulo Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} - # sa-east-1 region does not support the size instance we use. case "$aws_region" in 1) region="us-east-1" ;; @@ -271,6 +280,7 @@ Enter the number of your desired region: 11) region="eu-west-1" ;; 12) region="eu-west-2";; 13) region="ca-central-1" ;; + 14) region="sa-east-1" ;; esac ROLES="ec2 vpn cloud" From f300fdb60b048d775e7836ebbe8b7c874a9b28b3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 15 Apr 2017 16:33:22 +0200 Subject: [PATCH 441/769] Fixes #410 --- users.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/users.yml b/users.yml index 421c5814..b4cdf742 100644 --- a/users.yml +++ b/users.yml @@ -34,6 +34,7 @@ become: true vars_files: - config.cfg + - roles/vpn/defaults/main.yml pre_tasks: - name: Common pre-tasks From 02f363d8255841a982f311211da9821f69ec36b7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 15 Apr 2017 16:36:39 +0200 Subject: [PATCH 442/769] change the order of ciphers --- roles/vpn/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index db312818..934c34f3 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -25,5 +25,5 @@ ciphers: ike: aes128gcm16-sha2_512-prfsha512-ecp256! esp: aes128gcm16-sha2_512-ecp256! compat: - ike: aes128-sha2_512-prfsha512-ecp256,aes128gcm16-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! - esp: aes128-sha2_512-ecp256,aes128gcm16-sha2_512-ecp256,aes128-sha2_256-modp2048! + ike: aes128gcm16-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! + esp: aes128gcm16-sha2_512-ecp256,aes128-sha2_512-ecp256,aes128-sha2_256-modp2048! From 04b61ca3d2f4745a15a4f138232e5eda4ee23a05 Mon Sep 17 00:00:00 2001 From: MiWCryptAnalytics Date: Sat, 15 Apr 2017 16:23:15 -0400 Subject: [PATCH 443/769] Increase CA key entropy to 128bit (#415) Changes the default CA key size from 48 bit to 128bit with OpenSSL usermode CSPRNG with hex encoding --- roles/vpn/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 5ec7f3db..006479d7 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -8,7 +8,7 @@ - name: Generate password for the CA key shell: > - openssl rand -hex 6 + openssl rand -hex 16 register: CA_password - set_fact: From 42a663983e6c735d275a244408cfb635da356a12 Mon Sep 17 00:00:00 2001 From: donlockhart Date: Sun, 16 Apr 2017 08:39:55 -0400 Subject: [PATCH 444/769] Added East US and East US 2 regions to Azure. (#424) --- algo | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/algo b/algo index a9d4914c..f0bacbfa 100755 --- a/algo +++ b/algo @@ -137,6 +137,8 @@ Name the vpn server: 24. North Central US 25. South India 26. West India + 27. East US + 28. East US 2 Enter the number of your desired region: [1]: " -r azure_region @@ -169,6 +171,8 @@ Enter the number of your desired region: 24) region="northcentralus" ;; 25) region="southindia" ;; 26) region="westindia" ;; + 27) region="eastus" ;; + 28) region="eastus2" ;; esac ROLES="azure vpn cloud" From de948186eb12a96efe49a30ad47fb26e6f544389 Mon Sep 17 00:00:00 2001 From: Logan Collins Date: Sun, 16 Apr 2017 08:56:17 -0500 Subject: [PATCH 445/769] Improve Ubuntu Instructions (#419) * Added note regarding DH group * more complete * clarified file sources * remove trailing slash for consistency * Added information on LAN Passthrough - a common home usecase --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7572d6df..9002b1b8 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,28 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransf ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu Server, etc.) -Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. - #### Ubuntu Server 16.04 example -1. `/etc/ipsec.d/certs`: copy `user.crt` here -2. `/etc/ipsec.d/private`: copy `user.key` here -3. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key` -4. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update the value for `leftcert` -5. `sudo ipsec up `: start the ipsec tunnel -6. `sudo ipsec down `: shutdown the ipsec tunnel +1. Install Strongswan: `sudo apt-get install strongswan strongswan-plugin-openssl` Plugin required per [StrongSwan Documentation](https://wiki.strongswan.org/projects/strongswan/wiki/IKEv2CipherSuites), as the ECP_256 DH group is supported by the openssl plugin. +2. `/etc/ipsec.d/certs`: copy `user.crt` here from `algo-master/configs//pki/certs`. +3. `/etc/ipsec.d/private`: copy `user.key` here from `algo-master/configs//pki/private`. +4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` here from `algo-master/configs//cacert.pem`. +5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key`, like in `ipsec_user.secrets` but matching the `user.key` filename. +6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update the value for `leftcert` to match the `user.crt` filename. +7. `sudo ipsec restart`: pick up config changes +8. `sudo ipsec up `: start the ipsec tunnel +9. `sudo ipsec down `: shutdown the ipsec tunnel + +## LAN Passthrough + +To enable your device to access other devices on the LAN, add the following to `/etc/ipsec.conf`, replacing `192.168.1.1/24` with whatever subnet your LAN uses: + + conn lan-passthrough + leftsubnet=192.168.1.1/24 + rightsubnet=192.168.1.1/24 + authby=never # No authentication necessary + type=pass # passthrough + auto=route # no need to ipsec up lan-passthrough - it will just work ### Other Devices @@ -191,7 +203,6 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. -- [The Register](https://twitter.com/TheRegister/status/825076303657177088) > Algo is really easy and secure. - -- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) > I played around with Algo VPN, a set of scripts that let you set up a VPN in the cloud in very little time, even if you don’t know much about development. I’ve got to say that I was quite impressed with Trail of Bits’ approach. From 089bf64c91f5d239c9a4825cc890f9aeb26d2c70 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 10:00:57 -0400 Subject: [PATCH 446/769] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9002b1b8..1a9b19a1 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,21 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransf ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu Server, etc.) +Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. + #### Ubuntu Server 16.04 example -1. Install Strongswan: `sudo apt-get install strongswan strongswan-plugin-openssl` Plugin required per [StrongSwan Documentation](https://wiki.strongswan.org/projects/strongswan/wiki/IKEv2CipherSuites), as the ECP_256 DH group is supported by the openssl plugin. -2. `/etc/ipsec.d/certs`: copy `user.crt` here from `algo-master/configs//pki/certs`. -3. `/etc/ipsec.d/private`: copy `user.key` here from `algo-master/configs//pki/private`. -4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` here from `algo-master/configs//cacert.pem`. -5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key`, like in `ipsec_user.secrets` but matching the `user.key` filename. -6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update the value for `leftcert` to match the `user.crt` filename. +1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan +2. `/etc/ipsec.d/certs`: copy `user.crt` from `algo-master/configs//pki/certs` +3. `/etc/ipsec.d/private`: copy `user.key` from `algo-master/configs//pki/private` +4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//cacert.pem` +5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key` +6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update `leftcert` to match the `user.crt` filename 7. `sudo ipsec restart`: pick up config changes 8. `sudo ipsec up `: start the ipsec tunnel 9. `sudo ipsec down `: shutdown the ipsec tunnel -## LAN Passthrough - -To enable your device to access other devices on the LAN, add the following to `/etc/ipsec.conf`, replacing `192.168.1.1/24` with whatever subnet your LAN uses: +One common use case is to let your computer access your local LAN without going through the VPN. To enable your device to access other devices on the LAN, add the following to `/etc/ipsec.conf` and replace `192.168.1.1/24` with the subnet your LAN uses: conn lan-passthrough leftsubnet=192.168.1.1/24 From 3ef96f7848adfa9239df427836d2ca559397e39b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 10:02:34 -0400 Subject: [PATCH 447/769] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a9b19a1..92443886 100644 --- a/README.md +++ b/README.md @@ -144,14 +144,14 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, 8. `sudo ipsec up `: start the ipsec tunnel 9. `sudo ipsec down `: shutdown the ipsec tunnel -One common use case is to let your computer access your local LAN without going through the VPN. To enable your device to access other devices on the LAN, add the following to `/etc/ipsec.conf` and replace `192.168.1.1/24` with the subnet your LAN uses: +One common use case is to let your server access your local LAN without going through the VPN. Set up a passthrough connection by adding the following to `/etc/ipsec.conf`. Replace `192.168.1.1/24` with the subnet your LAN uses: conn lan-passthrough leftsubnet=192.168.1.1/24 rightsubnet=192.168.1.1/24 authby=never # No authentication necessary type=pass # passthrough - auto=route # no need to ipsec up lan-passthrough - it will just work + auto=route # no need to ipsec up lan-passthrough ### Other Devices From 38f85a6e78d7ad7df228e6aa401f49aa597d2853 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 10:12:07 -0400 Subject: [PATCH 448/769] Add Linux Desktop to compatible prompt --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index f0bacbfa..20b03a90 100755 --- a/algo +++ b/algo @@ -55,7 +55,7 @@ security_enabled=${security_enabled:-n} if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi read -p " -Do you want the VPN to support Windows 10 clients? (requires RSA certificates and key exchange, less secure) +Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) [y/N]: " -r Win10_Enabled Win10_Enabled=${Win10_Enabled:-n} if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi From 87316ea3ea837c3fdf74e7ae503b36c4bcb7e61c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 10:13:47 -0400 Subject: [PATCH 449/769] Add note about Network Manager --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 92443886..84d23de3 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ If you want to perform these steps by hand, you will need to import the user cer Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none ``` +### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) + +Network Manager does not support AES-GCM. In order to support Linux Desktop clients, please choose the "compatible" cryptography and use at least Network Manager 1.4.1. See [Issue #263](https://github.com/trailofbits/algo/issues/263) for more information. + ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu Server, etc.) Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. From bf75a1bb035d20cf478e8df1d9b6fccbfaef8411 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Apr 2017 16:18:54 +0200 Subject: [PATCH 450/769] move generating of the known_hosts file to local_action (#425) --- roles/ssh_tunneling/tasks/main.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 1cf23684..578fb793 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -53,9 +53,6 @@ ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null register: ssh_fingerprints -- name: The known_hosts file created - template: src=known_hosts.j2 dest=/root/.ssh/{{ IP_subject_alt_name }}_known_hosts - - name: Fetch users SSH private keys fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat=yes with_items: "{{ users }}" @@ -66,7 +63,11 @@ become: false - name: Fetch the known_hosts file - fetch: src='/root/.ssh/{{ IP_subject_alt_name }}_known_hosts' dest=configs/{{ IP_subject_alt_name }}/{{ IP_subject_alt_name }}_known_hosts flat=yes + local_action: + module: template + src: known_hosts.j2 + dest: configs/{{ IP_subject_alt_name }}/known_hosts + become: no - name: Build the client ssh config local_action: From 16329fe0883f6e29cd624d101a27fe33eb9c31cd Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Apr 2017 16:19:47 +0200 Subject: [PATCH 451/769] Instance size (#404) * Escaping Special Characters #388 * Make instance sizes more flexible to edit #355 --- config.cfg | 16 ++++++++++------ playbooks/local.yml | 2 +- roles/cloud-azure/tasks/main.yml | 4 ++-- roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/cloud-ec2/tasks/main.yml | 2 +- roles/cloud-gce/tasks/main.yml | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/config.cfg b/config.cfg index 747bae5f..b869dd2f 100644 --- a/config.cfg +++ b/config.cfg @@ -58,9 +58,13 @@ SSH_keys: private: configs/algo.pem public: configs/algo.pem.pub -dynamic_inventory_groups: - - azure - - digitalocean - - ec2 - - gce - - local +cloud_providers: + azure: + size: Basic_A0 + digitalocean: + size: 512mb + ec2: + size: t2.micro + gce: + size: f1-micro + local: diff --git a/playbooks/local.yml b/playbooks/local.yml index e852bc20..a7cc2d7e 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -19,6 +19,6 @@ create: yes block: | [algo:children] - {% for group in dynamic_inventory_groups %} + {% for group in cloud_providers.keys() %} {{ group }} {% endfor %} diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 17c6ce36..d3b831a8 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -80,7 +80,7 @@ virtual_network: algo_net name: "{{ azure_server_name }}" ssh_password_enabled: false - vm_size: Basic_A0 + vm_size: "{{ cloud_providers.azure.size }}" tags: Environment: Algo ssh_public_keys: @@ -91,7 +91,7 @@ sku: '16.04-LTS' version: latest register: azure_rm_virtualmachine - + # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? - set_fact: diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index a472fb56..28dd7f15 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -45,7 +45,7 @@ command: droplet name: "{{ do_server_name }}" region_id: "{{ do_region }}" - size_id: "512mb" + size_id: "{{ cloud_providers.digitalocean.size }}" image_id: "ubuntu-16-04-x64" ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" unique_name: yes diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index be0b0d4e..46a29425 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -90,7 +90,7 @@ keypair: "VPNKEY" vpc_subnet_id: "{{ vpc.subnets[0].id }}" group: vpn-secgroup - instance_type: t2.micro + instance_type: "{{ cloud_providers.ec2.size }}" image: "{{ ami_image }}" wait: true region: "{{ region }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 5c6a1f66..fce69ce3 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -13,7 +13,7 @@ gce: instance_names: "{{ server_name }}" zone: "{{ zone }}" - machine_type: f1-micro + machine_type: "{{ cloud_providers.gce.size }}" image: ubuntu-1604 service_account_email: "{{ service_account_email }}" credentials_file: "{{ credentials_file_path }}" From bdd0b854316331da66668601b84153a645b7933f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Apr 2017 16:40:10 +0200 Subject: [PATCH 452/769] Upgrade pip inside virtualenv. Fixes #409 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84d23de3..812c8095 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. ```bash - $ python -m virtualenv env && source env/bin/activate && python -m pip install -r requirements.txt + $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc` which you should accept. From 9a8f3d9dd01948f6e7436b0821b23124e2258221 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 11:10:11 -0400 Subject: [PATCH 453/769] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 812c8095..0cd16c8e 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. -- [The Register](https://twitter.com/TheRegister/status/825076303657177088) > Algo is really easy and secure. + -- [the grugq](https://twitter.com/thegrugq/status/786249040228786176) > I played around with Algo VPN, a set of scripts that let you set up a VPN in the cloud in very little time, even if you don’t know much about development. I’ve got to say that I was quite impressed with Trail of Bits’ approach. From 98efa75b6c4bb7ae4ef51f996932e0d94f4a9121 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 12:15:16 -0400 Subject: [PATCH 454/769] more endorsements! --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0cd16c8e..90992db1 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,10 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. -- [Romain Dillet](https://twitter.com/romaindillet/status/851037243728965632) for [TechCrunch](https://techcrunch.com/2017/04/09/how-i-made-my-own-vpn-server-in-15-minutes/) +> If you’re uncomfortable shelling out the cash to an anonymous, random VPN provider, this is the best solution. + +-- [Thorin Klosowski](https://twitter.com/kingthor) for [Lifehacker](http://lifehacker.com/how-to-set-up-your-own-completely-free-vpn-in-the-cloud-1794302432) + ## Support Algo VPN All donations support continued development. Thanks! From 32d906f04df47e35f6b1eb9fd92e512617c317f8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 13:58:06 -0400 Subject: [PATCH 455/769] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 90992db1..cce1f1b0 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,11 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/server-redhat-centos6.md) - Windows: See the [Windows documentation](docs/client-windows.md) -4. Install Algo's remaining dependencies for your operating system. Using the same terminal window as the previous step run the command below. +4. Install Algo's remaining dependencies for your operating system. Use the same terminal window as the previous step and run: ```bash $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt ``` - On macOS, you may be prompted to install `cc` which you should accept. + On macOS, you may be prompted to install `cc`. You should press accept. 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. From 4cd3c2e4ef3aca84c880f28e65cdb2e9e262f5d2 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Apr 2017 14:07:14 -0400 Subject: [PATCH 456/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cce1f1b0..0427e8c9 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua ```bash $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt ``` - On macOS, you may be prompted to install `cc`. You should press accept. + On macOS, you may be prompted to install `cc`. You should press accept if so. 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. From 9c12272c8cc80f5dc0ff2fc073f88cc70f7a0a6c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 16 Apr 2017 22:40:24 +0200 Subject: [PATCH 457/769] Python False-y values should be accepted. #417 (#426) --- roles/cloud-azure/tasks/main.yml | 52 +++++++++++++------------ roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/cloud-ec2/tasks/main.yml | 32 ++++++++------- roles/cloud-gce/tasks/main.yml | 2 +- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index d3b831a8..252894b6 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -2,13 +2,17 @@ - set_fact: resource_group: "Algo_{{ region }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" - name: Create a resource group azure_rm_resourcegroup: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" name: "{{ resource_group }}" location: "{{ region }}" tags: @@ -16,10 +20,10 @@ - name: Create a virtual network azure_rm_virtualnetwork: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" resource_group: "{{ resource_group }}" name: algo_net address_prefixes: "10.10.0.0/16" @@ -28,10 +32,10 @@ - name: Create a security group azure_rm_securitygroup: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" resource_group: "{{ resource_group }}" name: AlgoSecGroup purge_rules: yes @@ -57,10 +61,10 @@ - name: Create a subnet azure_rm_subnet: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" resource_group: "{{ resource_group }}" name: algo_subnet address_prefix: "10.10.0.0/24" @@ -71,10 +75,10 @@ - name: Create an instance azure_rm_virtualmachine: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" resource_group: "{{ resource_group }}" admin_username: ubuntu virtual_network: algo_net @@ -100,10 +104,10 @@ - name: Ensure the network interface includes all required parameters azure_rm_networkinterface: - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET')) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT')) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID')) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID')) }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" name: "{{ networkinterface_name }}" resource_group: "{{ resource_group }}" virtual_network_name: algo_net diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 28dd7f15..15fbbd9d 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,6 +1,6 @@ - name: Set the DigitalOcean Access Token fact set_fact: - do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN')) }}" + do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN'), true) }}" public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - block: diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 46a29425..dfb3b1f9 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,7 +1,11 @@ +- set_fact: + access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + - name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" owner: 099720109477 sort: creationDate @@ -18,8 +22,8 @@ - name: Add ssh public key ec2_key: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" name: VPNKEY region: "{{ region }}" key_material: "{{ item }}" @@ -28,8 +32,8 @@ - name: Configure EC2 virtual private clouds ec2_vpc: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" state: present resource_tags: { "Environment":"Algo" } region: "{{ region }}" @@ -42,8 +46,8 @@ - name: Set up Public Subnets Route Table ec2_vpc_route_table: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" vpc_id: "{{ vpc.vpc_id }}" region: "{{ region }}" state: present @@ -58,8 +62,8 @@ - name: Configure EC2 security group ec2_group: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" name: vpn-secgroup description: Security group for VPN servers region: "{{ region }}" @@ -85,8 +89,8 @@ - name: Launch instance ec2: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" keypair: "VPNKEY" vpc_subnet_id: "{{ vpc.subnets[0].id }}" group: vpn-secgroup @@ -120,8 +124,8 @@ - name: Get EC2 instances ec2_remote_facts: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" region: "{{ region }}" filters: instance-state-name: running diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index fce69ce3..08a380e4 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,5 +1,5 @@ - set_fact: - credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH')) }}" + credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - set_fact: From ea5976f49b3029751ac8b3298feae413c1521826 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 17 Apr 2017 18:12:38 +0200 Subject: [PATCH 458/769] write logs to file if BSD only --- roles/vpn/templates/strongswan.conf.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index 5e66cb2e..b658ac08 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -11,7 +11,7 @@ charon { } user = strongswan group = strongswan - +{% if ansible_distribution == 'FreeBSD' %} filelog { /var/log/charon.log { time_format = %b %e %T @@ -21,6 +21,7 @@ charon { flush_line = yes } } +{% endif %} } include strongswan.d/*.conf From f13cc718514f89e686b11252a8de1a903ab97459 Mon Sep 17 00:00:00 2001 From: George Kargiotakis Date: Mon, 17 Apr 2017 22:34:31 +0300 Subject: [PATCH 459/769] Simplify localhost installations (#432) Make it easier to install non_cloud version on localhost and add a check whether an IP was given for IP_subject --- algo | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/algo b/algo index 20b03a90..a89c704b 100755 --- a/algo +++ b/algo @@ -349,7 +349,8 @@ Please choose the number of your zone. Press enter for default (#8) zone. non_cloud () { read -p " Enter the IP address of your server: (or use localhost for local installation) -: " -r server_ip +[localhost]: " -r server_ip + server_ip=${server_ip:-localhost} read -p " @@ -357,11 +358,22 @@ What user should we use to login on the server? (note: passwordless login requir [root]: " -r server_user server_user=${server_user:-root} +if [ "x${server_ip}" = "xlocalhost" ]; then + myip="" +else + myip=${server_ip} +fi + read -p " Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$server_ip]: " -r IP_subject - IP_subject=${IP_subject:-$server_ip} +[$myip]: " -r IP_subject + IP_subject=${IP_subject:-$myip} + +if [ "x${IP_subject}" = "x" ]; then + echo "no server IP given. exiting." + exit 1 +fi ROLES="local vpn" EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" From aa0aadd66e36565d21464c44363f82be0b4b93d6 Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Mon, 17 Apr 2017 19:01:42 -0700 Subject: [PATCH 460/769] Removing update to ~/.ssh/config #400 (#435) --- .gitignore | 1 + README.md | 13 +++++++++++-- playbooks/local_ssh.yml | 11 ----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 7235538d..b68ae839 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ configs/* inventory_users *.kate-swp env +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 0427e8c9..09e2e34e 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,6 @@ You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to "\"#----------------------------------------------------------------------#\"", ``` -Note: If you want to run Algo again at any point in the future, you must first "reactivate" the dependencies for it. To reactivate them, open your terminal, use `cd` to navigate to the directory with Algo, then run `source env/bin/activate`. - Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/advanced-usage.md) documentation. ## Configure the VPN Clients @@ -177,6 +175,17 @@ Use the example command below to start an SSH tunnel by replacing `user` and `ip `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` +## SSH into Algo Server + +To SSH into the Algo server for administrative purposes you can use the example command below by replacing `ip` with your own: + + `ssh ubuntu@ip -i ~/.ssh/algo.pem` + +If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. + + `ssh-add ~/.ssh/algo > /dev/null 2>&1` + + ## Adding or Removing Users Algo's own scripts can easily add and remove users from the VPN server. diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml index fc754301..05e53d9a 100644 --- a/playbooks/local_ssh.yml +++ b/playbooks/local_ssh.yml @@ -12,14 +12,3 @@ src: "{{ SSH_keys.private }}" dest: ~/.ssh/algo.pem mode: '0600' - -- name: Configure the local ssh config - local_action: - module: blockinfile - dest: "~/.ssh/config" - marker: "# {mark} ALGO MANAGED BLOCK {{ cloud_instance_ip|default(server_ip) }}" - insertbefore: BOF - create: yes - block: | - Host {{ cloud_instance_ip|default(server_ip) }} - IdentityFile ~/.ssh/algo.pem From fa5a956193950fb7bb28d202efffbfdf187f4fea Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 18 Apr 2017 05:16:05 +0200 Subject: [PATCH 461/769] Add URLStringProbe (#428) * Add URLStringProbe * switch to Apple's hotspot-detect.html --- roles/vpn/templates/mobileconfig.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index b22bfa86..6d54233c 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -37,6 +37,8 @@ {% endif %} InterfaceTypeMatch WiFi + URLStringProbe + http://captive.apple.com/hotspot-detect.html Action @@ -47,6 +49,8 @@ {% endif %} InterfaceTypeMatch Cellular + URLStringProbe + http://captive.apple.com/hotspot-detect.html {% else %} From 5b2e13d18f4720792469c52285e2a63cd18b74cb Mon Sep 17 00:00:00 2001 From: Jauder Ho Date: Mon, 17 Apr 2017 20:17:40 -0700 Subject: [PATCH 462/769] Only enable ChaCha cipher (#412) * Only enable ChaCha cipher * Add back a few ciphers for compatability --- roles/security/templates/sshd_config.j2 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index 984f45c2..1485466b 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -45,10 +45,8 @@ HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key # Use only modern ciphers -KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 -Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com -MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com -# TODO: I haven't seen anyone review these yet -# HostKeyAlgorithms ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 -# TODO: I haven't seen anyone review these yet -# PubkeyAcceptedKeyTypes ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519 +KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256 +Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com +MACs hmac-sha2-256-etm@openssh.com +HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256 +# PubkeyAcceptedKeyTypes accept anything From 14e8f309fe33f4e31c9a9a56976a2d539449872a Mon Sep 17 00:00:00 2001 From: MiWCryptAnalytics Date: Mon, 17 Apr 2017 23:41:04 -0400 Subject: [PATCH 463/769] Update troubleshooting with note about ip frag (#427) * Update troubleshooting with note about ip frag note about ip fragmentation on consumer routers * clarify Closes #305 --- docs/troubleshooting.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 273a0ffe..3c055573 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -14,7 +14,8 @@ 10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) 11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-the-algo-server) 12. [I can't get Network Manager to connect to the Algo Server](#12-i-cant-get-network-manager-to-connect-to-the-algo-server) -13. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +13. [IKEAUTH request never makes it to the server](#13-ikeauth-request-never-makes-it-to-the-server) +14. [I have a problem not covered here](#i-have-a-problem-not-covered-here) ### 1. Error: "You have not agreed to the Xcode license agreements" @@ -130,6 +131,12 @@ In order to connect to the Algo VPN server, your router must support IKEv2, ECC You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263) without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. +### 13. "Error 809" or IKE_AUTH requests that never make it to the server + +On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. + +It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with 'Block Fragmented IP packets'. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). + ### I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#algo-support** channel. From 8e5e6d50884cab651ace243d1e0685082150a8db Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 18 Apr 2017 01:11:56 -0400 Subject: [PATCH 464/769] remove extraneous integrity algos from AEAD ciphers (#439) In reference to https://github.com/trailofbits/algo/issues/9#issuecomment-294370560 --- roles/vpn/defaults/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 934c34f3..120d1dc0 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -22,8 +22,8 @@ strongswan_enabled_plugins: ciphers: defaults: - ike: aes128gcm16-sha2_512-prfsha512-ecp256! - esp: aes128gcm16-sha2_512-ecp256! + ike: aes128gcm16-prfsha512-ecp256! + esp: aes128gcm16-ecp256! compat: - ike: aes128gcm16-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! - esp: aes128gcm16-sha2_512-ecp256,aes128-sha2_512-ecp256,aes128-sha2_256-modp2048! + ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-modp2048! + esp: aes128gcm16-ecp256,aes128-sha2_512-ecp256,aes128-sha2_512-prfsha512-modp2048! From 1778cb1f45e31c803a0cf30edc76b55cb0c91e83 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 18 Apr 2017 01:12:21 -0400 Subject: [PATCH 465/769] disable dpd #430 (#437) Closes #430 --- roles/vpn/templates/ipsec.conf.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 03211b94..ec744802 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -1,11 +1,11 @@ config setup - uniqueids = never # allow multiple connections per user + uniqueids = replace charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" conn %default fragmentation=yes rekey=no - dpdaction=clear + dpdaction=none keyexchange=ikev2 compress=yes dpddelay=35s From f9f7be7b0d9c533af2362caa235ef37e3adec09b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 18 Apr 2017 01:15:07 -0400 Subject: [PATCH 466/769] Fix a typo from #439 --- roles/vpn/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 120d1dc0..0a23f248 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -26,4 +26,4 @@ ciphers: esp: aes128gcm16-ecp256! compat: ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-modp2048! - esp: aes128gcm16-ecp256,aes128-sha2_512-ecp256,aes128-sha2_512-prfsha512-modp2048! + esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-modp2048! From b29772f1466daeef19c7ac2f5dd9e6ed9aeb7fab Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 18 Apr 2017 02:20:44 -0400 Subject: [PATCH 467/769] prefer ed25519 --- roles/security/templates/sshd_config.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index 1485466b..daddbed1 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -41,8 +41,8 @@ PrintMotd no PrintLastLog yes # Use only modern host keys -HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key +HostKey /etc/ssh/ssh_host_ecdsa_key # Use only modern ciphers KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256 From 77700f6c8e93e78d9a80d7bc3106b2accdc1adfa Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 18 Apr 2017 11:22:38 -0400 Subject: [PATCH 468/769] clarification about ciphers --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 09e2e34e..2774b767 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC ## Features -* Supports only IKEv2, with a single cipher suite: AES-GCM, HMAC-SHA2, and P-256 DH +* Supports only IKEv2 with strong crypto: AES-GCM, SHA2, and P-256 * Generates Apple profiles to auto-configure iOS and macOS devices -* Includes helper scripts to add and remove users +* Includes a helper script to add and remove users * Blocks ads with a local DNS resolver and HTTP proxy (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Google Compute Engine, Microsoft Azure, or your own server +* Installs to DigitalOcean, Amazon EC2, Microsoft Azure, Google Compute Engine, or your own server ## Anti-features From 8173b84ff8622bd21509aa16f85b0922052a5f37 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 19 Apr 2017 03:53:30 -0400 Subject: [PATCH 469/769] Change uniqueids back to never (#448) We need this to allow multiple connections with the same id/certificate --- roles/vpn/templates/ipsec.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index ec744802..9a325526 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -1,5 +1,5 @@ config setup - uniqueids = replace + uniqueids=never # allow multiple connections per user charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" conn %default From 0b05ea19bc253e479fff585c0be7e5b23d934b30 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 20 Apr 2017 07:26:46 -0400 Subject: [PATCH 470/769] Windows needs SHA2-256. Closes #453. (#456) --- roles/vpn/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 0a23f248..d4e9bfd4 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -25,5 +25,5 @@ ciphers: ike: aes128gcm16-prfsha512-ecp256! esp: aes128gcm16-ecp256! compat: - ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-modp2048! - esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_512-prfsha512-modp2048! + ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! + esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! From 019d729fe652074153de6049df184dc7c5ebe5af Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 20 Apr 2017 17:56:03 -0400 Subject: [PATCH 471/769] Better documentation (#459) * Closes #443 * Remove numbers * context * split up local and scripted * Closes #458 * . * better layout * Closes #451 * do this later * grammar * typo --- README.md | 20 ++- docs/cloud-unsupported.md | 20 +++ ...server-freebsd.md => deploy-to-freebsd.md} | 2 +- docs/deploy-to-ubuntu.md | 14 +++ ...vanced-usage.md => deploy-with-ansible.md} | 22 +--- docs/index.md | 24 ++-- ...hat-centos6.md => setup-redhat-centos6.md} | 0 docs/{ansible-roles.md => setup-roles.md} | 0 docs/troubleshooting.md | 116 +++++++++++------- 9 files changed, 138 insertions(+), 80 deletions(-) create mode 100644 docs/cloud-unsupported.md rename docs/{server-freebsd.md => deploy-to-freebsd.md} (83%) create mode 100644 docs/deploy-to-ubuntu.md rename docs/{advanced-usage.md => deploy-with-ansible.md} (66%) rename docs/{server-redhat-centos6.md => setup-redhat-centos6.md} (100%) rename docs/{ansible-roles.md => setup-roles.md} (100%) diff --git a/README.md b/README.md index 2774b767..5f9ddaf3 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,22 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation -* [Advanced Usage](docs/advanced-usage.md) describes how to deploy an Algo VPN server directly from Ansible. -* [FAQ](docs/faq.md) includes answers to common questions. -* [Roles](docs/ansible-roles.md) includes a description of optional Algo VPN server features. -* [Troubleshooting](docs/troubleshooting.md) includes answers to common technical issues. +* Setup instructions + - Documentation for avaialble [Ansible roles](setup-roles.md) + - Deploy from [RedHat/CentOS 6.x](setup-redhat-centos6.md) +* Client setup + - Setup [Windows](client-windows.md) clients + - Setup [Android](client-android.md) clients + - Setup [Generic/Linux](client-generic.md) clients with Ansible +* Cloud setup + - Configure [Azure](cloud-azure.md) + - Deploy to an [unsupported cloud provider](cloud-unsupported.md) +* Advanced Deployment + - Deploy to local [FreeBSD](deploy-to-freebsd.md) servers + - Deploy to local [Ubuntu 16.04](deploy-to-ubuntu.md) servers + - Deploy with [Ansible](deploy-with-ansible.md) +* [FAQ](faq.md) +* [Troubleshooting](troubleshooting.md) ## Endorsements diff --git a/docs/cloud-unsupported.md b/docs/cloud-unsupported.md new file mode 100644 index 00000000..82cc2ce7 --- /dev/null +++ b/docs/cloud-unsupported.md @@ -0,0 +1,20 @@ +# Unsupported Cloud Providers + +Algo officially supports DigitalOcean, Amazon Web Services, Microsoft Azure, and Google Cloud Engine. If you want to deploy Algo on another virtual hosting provider, that provider must support: + +1. the base operating system image that Algo uses (Ubuntu 16.04), and +2. a minimum of certain kernel modules required for the strongSwan IPsec server. + +Please see the [Required Kernel Modules](https://wiki.strongswan.org/projects/strongswan/wiki/KernelModules) documentation from strongSwan for a list of the specific required modules and a script to check for them. As a first step, we recommend running their shell script to determine initial compatibility with your new hosting provider. + +If you want Algo to officially support your new cloud provider then it must have an Ansible [cloud module](https://docs.ansible.com/ansible/list_of_cloud_modules.html) available. If no module is available for your provider, search Ansible's [open issues](https://github.com/ansible/ansible/issues) and [pull requests](https://github.com/ansible/ansible/pulls) for existing efforts to add it. If none are available, then you may want to develop the module yourself. Reference the [Ansible module developer documentation](https://docs.ansible.com/ansible/dev_guide/developing_modules.html) and the API documentation for your hosting provider. + +## IPsec in userland + +Hosting providers that rely on OpenVZ or Docker cannot be used by Algo since they cannot load the required kernel modules or access the required network interfaces. For more information, see the strongSwan documentation on [Cloud Platforms](https://wiki.strongswan.org/projects/strongswan/wiki/Cloudplatforms). + +In order to address this issue, strongSwan has developed the [kernel-libipsec](https://wiki.strongswan.org/projects/strongswan/wiki/Kernel-libipsec) plugin which provides an IPsec backend that works entirely in userland. `libipsec` bundles its own IPsec implementation and uses TUN devices to route packets. For example, `libipsec` is used by the Android strongSwan app to address Adnroid's lack of a functional IPsec stack. + +Use of `libipsec` is not supported by Algo. It has known performance issues since it buffers each packet in memory. On certain systems with insufficient processor power, such as many cloud hosting providers, using `libipsec` can lead to an out of memory conditions, crash the charon daemon, or lock up the entire host. + +Further, `libipsec` introduces unknown security risks. The code in `libipsec` has not been scrutinized to the same level as the code in the Linux or FreeBSD kernel that it replaces. This additional code introduces new complexity to the Algo server that we want to avoid at this time. We recommend moving to a hosting provider that does not require libipsec. \ No newline at end of file diff --git a/docs/server-freebsd.md b/docs/deploy-to-freebsd.md similarity index 83% rename from docs/server-freebsd.md rename to docs/deploy-to-freebsd.md index f78f4b0e..71440cc5 100644 --- a/docs/server-freebsd.md +++ b/docs/deploy-to-freebsd.md @@ -1,6 +1,6 @@ # FreeBSD / HardenedBSD server setup -It is only possible to install Algo on existing systems only. We support only 11 version for now. +FreeBSD server support is a work in progress. For now, it is only possible to install Algo on existing FreeBSD 11 systems. ## System preparation diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md new file mode 100644 index 00000000..d25aaca8 --- /dev/null +++ b/docs/deploy-to-ubuntu.md @@ -0,0 +1,14 @@ +# Local deployment + +It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install Ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. + +tl;dr: + +```shell +sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible +sudo apt-get update && sudo apt-get install ansible +git clone https://github.com/trailofbits/algo +cd algo && ./algo +``` + +**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. \ No newline at end of file diff --git a/docs/advanced-usage.md b/docs/deploy-with-ansible.md similarity index 66% rename from docs/advanced-usage.md rename to docs/deploy-with-ansible.md index 5f1b18a9..e1a86ff2 100644 --- a/docs/advanced-usage.md +++ b/docs/deploy-with-ansible.md @@ -1,23 +1,6 @@ -# Advanced Usage +# Scripted Deployment -Make sure you have installed all the dependencies necessary for your operating system as described in the [README](../README.md). - -## Local deployment - -It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install Ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. - -tl;dr: - -```shell -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible -git clone https://github.com/trailofbits/algo -cd algo && ./algo -``` - -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. - -## Scripted deployment +Before you begin, make sure you have installed all the dependencies necessary for your operating system as described in the [README](../README.md). You can deploy Algo non-interactively by running the Ansible playbooks directly with `ansible-playbook`. @@ -47,7 +30,6 @@ Server roles: - role: vpn, tags: vpn - role: dns_adblocking, tags: dns, adblock -- role: proxy, tags: proxy, adblock - role: security, tags: security - role: ssh_tunneling, tags: ssh_tunneling diff --git a/docs/index.md b/docs/index.md index 24dd4aac..37d4f73f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,15 +1,19 @@ # Algo VPN documentation -* [Advanced usage](advanced-usage.md) -* [Ansible roles](ansible-roles.md) +* Setup instructions + - Documentation for avaialble [Ansible roles](setup-roles.md) + - Deploy from [RedHat/CentOS 6.x](setup-redhat-centos6.md) * Client setup - - [Windows](client-windows.md) - - [Android](client-android.md) - - [Generic/Linux](client-generic.md) + - Setup [Windows](client-windows.md) clients + - Setup [Android](client-android.md) clients + - Setup [Generic/Linux](client-generic.md) clients with Ansible * Cloud setup - - [Azure](cloud-azure.md) -* Server setup - - [RedHat/CentOS 6.x](server-redhat-centos6.md) - - [FreeBSD](server-freebsd.md) -* [Troubleshooting](troubleshooting.md) + - Configure [Azure](cloud-azure.md) + - Deploy to an [unsupported cloud provider](cloud-unsupported.md) +* Advanced Deployment + - Deploy to local [FreeBSD](deploy-to-freebsd.md) servers + - Deploy to local [Ubuntu 16.04](deploy-to-ubuntu.md) servers + - Deploy with [Ansible](deploy-with-ansible.md) * [FAQ](faq.md) +* [Troubleshooting](troubleshooting.md) + diff --git a/docs/server-redhat-centos6.md b/docs/setup-redhat-centos6.md similarity index 100% rename from docs/server-redhat-centos6.md rename to docs/setup-redhat-centos6.md diff --git a/docs/ansible-roles.md b/docs/setup-roles.md similarity index 100% rename from docs/ansible-roles.md rename to docs/setup-roles.md diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3c055573..a72c500a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,23 +1,10 @@ # Troubleshooting -## Table of Contents +## Installation Problems -1. [Error: "You have not agreed to the Xcode license agreements"](#1-error-you-have-not-agreed-to-the-xcode-license-agreements) -2. [Error: "fatal error: 'openssl/opensslv.h' file not found"](#2-error-fatal-error-opensslopensslvh-file-not-found) -3. [Error: "TypeError: must be str, not bytes"](#3-error-typeerror-must-be-str-not-bytes) -4. [Error: "ansible-playbook: command not found"](#4-error-ansible-playbook-command-not-found) -5. [Bad owner or permissions on .ssh](#5-bad-owner-or-permissions-on-ssh) -6. [Little Snitch is broken when connected to the VPN](#6-little-snitch-is-broken-when-connected-to-the-vpn) -7. [Various websites appear to be offline through the VPN](#7-various-websites-appear-to-be-offline-through-the-vpn) -8. [The region you want is not available](#8-the-region-you-want-is-not-available) -9. [I want to change the list of trusted Wifi networks on my Apple device](#9-i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) -10. [Error: "The VPN Service payload could not be installed"](#10-error-the-vpn-service-payload-could-not-be-installed) -11. [I can't get my router to connect to the Algo server](#11-i-cant-get-my-router-to-connect-to-the-algo-server) -12. [I can't get Network Manager to connect to the Algo Server](#12-i-cant-get-network-manager-to-connect-to-the-algo-server) -13. [IKEAUTH request never makes it to the server](#13-ikeauth-request-never-makes-it-to-the-server) -14. [I have a problem not covered here](#i-have-a-problem-not-covered-here) +Look here if you have a problem running the installer to set up a new Algo server. -### 1. Error: "You have not agreed to the Xcode license agreements" +### Error: "You have not agreed to the Xcode license agreements" On macOS, you tried to install the dependencies with pip and encountered the following error: @@ -41,7 +28,33 @@ Storing debug log for failure in /Users/algore/Library/Logs/pip.log The Xcode compiler is installed but requires you to accept its license agreement prior to using it. Run `xcodebuild -license` to agree and then retry installing the dependencies. -### 2. Error: "fatal error: 'openssl/opensslv.h' file not found" +### Error: checking whether the C compiler works... no + +On macOS, you tried to install the dependencies with pip and encountered the following error: + +``` +Failed building wheel for pycrypto +Running setup.py clean for pycrypto +Failed to build pycrypto +... +copying lib/Crypto/Signature/PKCS1_v1_5.py -> build/lib.macosx-10.6-intel-2.7/Crypto/Signature +running build_ext +running build_configure +checking for gcc... gcc +checking whether the C compiler works... no +configure: error: in '/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto': configure: error: C compiler cannot create executables See config.log for more details +Traceback (most recent call last): +File "", line 1, in +... +cmd_obj.run() +File "/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto/setup.py", line 278, in run +raise RuntimeError("autoconf error") +RuntimeError: autoconf error +``` + +You don't have a working compiler installed. You should install the XCode compiler by opening your terminal and running `xcode-select --install`. + +### Error: "fatal error: 'openssl/opensslv.h' file not found" On macOS, you tried to install pycrypto and encountered the following error: @@ -62,9 +75,9 @@ Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/p Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. +You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. -### 3. Error: "TypeError: must be str, not bytes" +### Error: "TypeError: must be str, not bytes" You tried to install Algo and you see many repeated errors referencing `TypeError`, such as `TypeError: '>=' not supported between instances of 'TypeError' and 'int'` and `TypeError: must be str, not bytes`. For example: @@ -76,13 +89,13 @@ fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, " You may be trying to run Algo with Python3. Algo uses [Ansible](https://github.com/ansible/ansible) which has issues with Python3, although this situation is improving over time. Try running Algo with Python2 to fix this issue. Open your terminal and `cd` to the directory with Algo, then run: ``virtualenv -p `which python2.7` env && source env/bin/activate && pip install -r requirements.txt`` -### 4. Error: "ansible-playbook: command not found" +### Error: "ansible-playbook: command not found" You tried to install Algo and you see an error that reads "ansible-playbook: command not found." You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. -### 5. Bad owner or permissions on .ssh +### Bad owner or permissions on .ssh You tried to run Algo and it quickly exits with an error about a bad owner or permissions: @@ -92,11 +105,44 @@ fatal: [104.236.2.94]: UNREACHABLE! => {"changed": false, "msg": "Failed to conn You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home/user/.ssh` and then `chmod 600 /home/user/.ssh/config`. You may need to repeat this for other files mentioned in the error message. -### 6. Little Snitch is broken when connected to the VPN +### The region you want is not available + +You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. + + +## Connection Problems + +Look here if you deployed an Algo server but now have a problem connecting to it with a client. + +### I'm blocked or get CAPTCHAs when I access certain websites + +This is normal. + +When you deploy a Algo to a new cloud server, the address you are given may have been used before. In some cases, a malicious individual may have attacked others with that address and had it added to "IP reputation" feeds or simply a blacklist. In order to regain the trust for that address, you may be asked to enter CAPTCHAs to prove that you are a human, and not a Denial of Service (DoS) bot trying to attack others. This happens most frequently with Google. You can try entering the CAPTCHAs or you can try redeploying your Algo server to a new IP to resolve this issue. + +In some cases, a website will block any visitors accessing their site through a cloud hosting provider due to previous, frequent DoS attacks originating from them. In these cases, there is not much you can do except deploy Algo to your own server or another IP that the website has not outright blocked. + +### I want to change the list of trusted Wifi networks on my Apple device + +This setting is enforced on your client device via the Apple profile you put on it. You can edit the profile with new settings, then load it on your device to change the settings. You can use the [Apple Configurator](https://itunes.apple.com/us/app/apple-configurator-2/id1037126344?mt=12) to edit and resave the profile. Advanced users can edit the file directly in a text editor. Use the [Configuration Profile Reference](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html) for information about the file format and other available options. If you're not comfortable editing the profile, you can also simply redeploy a new Algo server with different settings to receive a new auto-generated profile. + +### Error: "The VPN Service payload could not be installed." + +You tried to install the Apple profile on one of your devices and you received an error stating `The "VPN Service" payload could not be installed. The VPN service could not be created.` Client support for Algo VPN is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+. Please upgrade your operating system and try again. + +### Little Snitch is broken when connected to the VPN Little Snitch is not compatible with IPSEC VPNs due to a known bug in macOS and there is no solution. The Little Snitch "filter" does not get incoming packets from IPSEC VPNs and, therefore, cannot evaluate any rules over them. Their developers have filed a bug report with Apple but there has been no response. There is nothing they or Algo can do to resolve this problem on their own. You can read more about this problem in [issue #134](https://github.com/trailofbits/algo/issues/134). -### 7. Various websites appear to be offline through the VPN +### I can't get my router to connect to the Algo server + +In order to connect to the Algo VPN server, your router must support IKEv2, ECC certificate-based authentication, and the cipher suite we use. See the ipsec.conf files we generate in the `config` folder for more information. Note that we do not officially support routers as clients for Algo VPN at this time, though patches and documentation for them are welcome (for example, see open issues for [Ubiquiti](https://github.com/trailofbits/algo/issues/307) and [pfSense](https://github.com/trailofbits/algo/issues/292)). + +### I can't get Network Manager to connect to the Algo server + +You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263) without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. + +### Various websites appear to be offline through the VPN This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. @@ -111,32 +157,12 @@ Then, set the MTU size on your network adapter (wlan0 or eth0): $ sudo ifconfig wlan0 mtu 1438 ``` -### 8. The region you want is not available - -You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. - -### 9. I want to change the list of trusted Wifi networks on my Apple device - -This setting is enforced on your client device via the Apple profile you put on it. You can edit the profile with new settings, then load it on your device to change the settings. You can use the [Apple Configurator](https://itunes.apple.com/us/app/apple-configurator-2/id1037126344?mt=12) to edit and resave the profile. Advanced users can edit the file directly in a text editor. Use the [Configuration Profile Reference](https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html) for information about the file format and other available options. If you're not comfortable editing the profile, you can also simply redeploy a new Algo server with different settings to receive a new auto-generated profile. - -### 10. Error: "The VPN Service payload could not be installed." - -You tried to install the Apple profile on one of your devices and you received an error stating `The "VPN Service" payload could not be installed. The VPN service could not be created.` Client support for Algo VPN is limited to modern operating systems, e.g. macOS 10.11+, iOS 9+. Please upgrade your operating system and try again. - -### 11. I can't get my router to connect to the Algo server - -In order to connect to the Algo VPN server, your router must support IKEv2, ECC certificate-based authentication, and the cipher suite we use. See the ipsec.conf files we generate in the `config` folder for more information. Note that we do not officially support routers as clients for Algo VPN at this time, though patches and documentation for them are welcome (for example, see open issues for [Ubiquiti](https://github.com/trailofbits/algo/issues/307) and [pfSense](https://github.com/trailofbits/algo/issues/292)). - -### 12. I can't get Network Manager to connect to the Algo server - -You're trying to connect Ubuntu or Debian to the Algo server through the Network Manager GUI but it's not working. Many versions of Ubuntu and some older versions of Debian bundle a [broken version of Network Manager](https://github.com/trailofbits/algo/issues/263) without support for modern standards or the strongSwan server. You must upgrade to Ubuntu 17.04 or Debian 9 Stretch, each of which contain the required minimum version of Network Manager. - -### 13. "Error 809" or IKE_AUTH requests that never make it to the server +### "Error 809" or IKE_AUTH requests that never make it to the server On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with 'Block Fragmented IP packets'. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). -### I have a problem not covered here +## I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#algo-support** channel. From a7b06058cb2a83b20feb9990c3cc42e77022abf5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 21 Apr 2017 00:00:17 +0200 Subject: [PATCH 472/769] remove the proxy role #440 (#457) * remove the proxy role #440 * Separate facts. Make roles more independent from each other move openssl to local tasks move unneeded tasks --- README.md | 8 +- config.cfg | 4 +- deploy.yml | 5 - docs/setup-roles.md | 4 - playbooks/common.yml | 7 +- playbooks/facts/main.yml | 42 + roles/dns_adblocking/tasks/main.yml | 3 +- roles/proxy/handlers/main.yml | 12 - roles/proxy/meta/main.yml | 5 - roles/proxy/tasks/main.yml | 115 - roles/proxy/templates/000-default.conf.j2 | 11 - .../apache2_100-CustomLimitations.conf.j2 | 4 - roles/proxy/templates/default.filter.j2 | 0 roles/proxy/templates/pagespeed.conf.j2 | 369 --- roles/proxy/templates/ports.conf.j2 | 13 - .../privoxy_100-CustomLimitations.conf.j2 | 4 - roles/proxy/templates/privoxy_config.j2 | 2107 ----------------- roles/proxy/templates/usr.sbin.privoxy.j2 | 15 - roles/security/tasks/main.yml | 2 + roles/ssh_tunneling/tasks/main.yml | 3 - roles/vpn/tasks/main.yml | 23 +- users.yml | 4 - 22 files changed, 53 insertions(+), 2707 deletions(-) create mode 100644 playbooks/facts/main.yml delete mode 100644 roles/proxy/handlers/main.yml delete mode 100644 roles/proxy/meta/main.yml delete mode 100644 roles/proxy/tasks/main.yml delete mode 100644 roles/proxy/templates/000-default.conf.j2 delete mode 100644 roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 delete mode 100644 roles/proxy/templates/default.filter.j2 delete mode 100644 roles/proxy/templates/pagespeed.conf.j2 delete mode 100644 roles/proxy/templates/ports.conf.j2 delete mode 100644 roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 delete mode 100644 roles/proxy/templates/privoxy_config.j2 delete mode 100644 roles/proxy/templates/usr.sbin.privoxy.j2 diff --git a/README.md b/README.md index 5f9ddaf3..635ae52a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Supports only IKEv2 with strong crypto: AES-GCM, SHA2, and P-256 * Generates Apple profiles to auto-configure iOS and macOS devices * Includes a helper script to add and remove users -* Blocks ads with a local DNS resolver and HTTP proxy (optional) +* Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan * Installs to DigitalOcean, Amazon EC2, Microsoft Azure, Google Compute Engine, or your own server @@ -79,7 +79,7 @@ You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to "\"# Config files and certificates are in the ./configs/ directory. #\"", "\"# Go to https://whoer.net/ after connecting #\"", "\"# and ensure that all your traffic passes through the VPN. #\"", - "\"# Local DNS resolver and Proxy IP address: 172.16.0.1 #\"", + "\"# Local DNS resolver 172.16.0.1 #\"", "\"# The p12 and SSH keys password is XXXXXXXX #\"", "\"#----------------------------------------------------------------------#\"", ``` @@ -180,9 +180,9 @@ Use the example command below to start an SSH tunnel by replacing `user` and `ip To SSH into the Algo server for administrative purposes you can use the example command below by replacing `ip` with your own: `ssh ubuntu@ip -i ~/.ssh/algo.pem` - + If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. - + `ssh-add ~/.ssh/algo > /dev/null 2>&1` diff --git a/config.cfg b/config.cfg index b869dd2f..b0fdefe0 100644 --- a/config.cfg +++ b/config.cfg @@ -28,7 +28,7 @@ dns_servers: - 2001:4860:4860::8888 - 2001:4860:4860::8844 -# IP address for the proxy and the local dns resolver +# IP address for the local dns resolver local_service_ip: 172.16.0.1 pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" @@ -45,7 +45,7 @@ congrats: "# Config files and certificates are in the ./configs/ directory. #" "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver and Proxy IP address: {{ local_service_ip }} #" + "# Local DNS resolver {{ local_service_ip }} #" p12_pass: | "# The p12 and SSH keys password is {{ easyrsa_p12_export_password }} #" ca_key_pass: | diff --git a/deploy.yml b/deploy.yml index 3d6966e1..5623c253 100644 --- a/deploy.yml +++ b/deploy.yml @@ -41,13 +41,8 @@ include: playbooks/common.yml tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] - - set_fact: - cloud_deployment: true - tags: ['cloud'] - roles: - { role: security, tags: [ 'security' ] } - - { role: proxy, tags: [ 'proxy', 'adblock' ] } - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - { role: vpn, tags: [ 'vpn' ] } diff --git a/docs/setup-roles.md b/docs/setup-roles.md index 2e224c13..697fc5f9 100644 --- a/docs/setup-roles.md +++ b/docs/setup-roles.md @@ -17,10 +17,6 @@ * Enables [unattended-upgrades](https://help.ubuntu.com/community/AutomaticSecurityUpdates) to ensure available patches are always applied * Modify features like core dumps, kernel parameters, and SUID binaries to limit possible attacks * Enhances SSH with modern ciphers and seccomp, and restricts access to old or unwanted features like X11 forwarding and SFTP -* **Proxy-based Adblocking and Compression** - * Installs [Privoxy](https://www.privoxy.org/) with an ad blocking ruleset - * Installs Apache with [mod_pagespeed](http://modpagespeed.com/) as an HTTP proxy - * Constrains Privoxy and Apache with AppArmor and cgroups CPU and memory limitations * **DNS-based Adblocking** * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations diff --git a/playbooks/common.yml b/playbooks/common.yml index 3308fa7b..04a3966c 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -12,9 +12,4 @@ include: freebsd.yml when: '"FreeBSD" in OS.stdout' -- name: Ensure the algo ssh key exist on the server - authorized_key: - user: "{{ ansible_ssh_user }}" - state: present - key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - tags: [ 'cloud' ] +- include: facts/main.yml diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml new file mode 100644 index 00000000..d66f15c4 --- /dev/null +++ b/playbooks/facts/main.yml @@ -0,0 +1,42 @@ +--- + +- name: Gather Facts + setup: + +- name: Ensure the algo ssh key exist on the server + authorized_key: + user: "{{ ansible_ssh_user }}" + state: present + key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + tags: [ 'cloud' ] + +- name: Enable IPv6 + set_fact: + ipv6_support: true + when: ansible_default_ipv6.gateway is defined + +- name: Set facts if the deployment in a cloud + set_fact: + cloud_deployment: true + tags: ['cloud'] + +- name: Generate password for the CA key + local_action: + module: shell + openssl rand -hex 16 + become: no + register: CA_password + +- name: Define password facts + set_fact: + easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" + easyrsa_CA_password: "{{ CA_password.stdout }}" + +- name: Define the commonName + set_fact: + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Change the algorithm to RSA + set_fact: + algo_params: "rsa:2048" + when: Win10_Enabled is defined and Win10_Enabled == "Y" diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 90a86ee3..f2f0aeb3 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,5 +1,4 @@ -- name: Gather Facts - setup: +--- - name: Dnsmasq installed package: name=dnsmasq diff --git a/roles/proxy/handlers/main.yml b/roles/proxy/handlers/main.yml deleted file mode 100644 index a31941ba..00000000 --- a/roles/proxy/handlers/main.yml +++ /dev/null @@ -1,12 +0,0 @@ -- name: restart privoxy - service: name=privoxy state=restarted - -- name: daemon-reload - shell: systemctl daemon-reload - -- name: restart apparmor - service: name=apparmor state=restarted - -- name: restart apache2 - service: name=apache2 state=restarted - diff --git a/roles/proxy/meta/main.yml b/roles/proxy/meta/main.yml deleted file mode 100644 index ef71a470..00000000 --- a/roles/proxy/meta/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } - - { role: vpn, tags: vpn } diff --git a/roles/proxy/tasks/main.yml b/roles/proxy/tasks/main.yml deleted file mode 100644 index 0af30dfc..00000000 --- a/roles/proxy/tasks/main.yml +++ /dev/null @@ -1,115 +0,0 @@ -- name: Gather Facts - setup: - -- name: Privoxy installed - apt: name=privoxy state=latest - -- name: Privoxy configured - template: src="{{ item.src }}" dest="{{ item.dest }}" - with_items: - - { src: privoxy_config.j2, dest: /etc/privoxy/config } - - { src: default.filter.j2, dest: /etc/privoxy/default.filter } - notify: - - restart privoxy - -- name: Privoxy profile for apparmor configured - template: src=usr.sbin.privoxy.j2 dest=/etc/apparmor.d/usr.sbin.privoxy owner=root group=root mode=0600 - when: apparmor_enabled is defined and apparmor_enabled == true - notify: - - restart privoxy - -- name: Enforce the privoxy AppArmor policy - shell: aa-enforce usr.sbin.privoxy - when: apparmor_enabled is defined and apparmor_enabled == true - tags: ['apparmor'] - -- name: Ensure that the privoxy service directory exist - file: path=/etc/systemd/system/privoxy.service.d/ state=directory mode=0755 owner=root group=root - -- name: Setup the cgroup limitations for the privoxy daemon - template: src=privoxy_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/privoxy.service.d/100-CustomLimitations.conf - notify: - - daemon-reload - - restart privoxy - -- meta: flush_handlers - -- name: Privoxy enabled and started - service: name=privoxy state=started enabled=yes - -# PageSpeed - -- name: Apache installed - apt: name=apache2 state=latest - -- name: PageSpeed installed for x86_64 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_amd64.deb - when: ansible_architecture == "x86_64" - -- name: PageSpeed installed for i386 - apt: deb=https://dl-ssl.google.com/dl/linux/direct/mod-pagespeed-stable_current_i386.deb - when: ansible_architecture != "x86_64" - -- name: PageSpeed configured - template: src=pagespeed.conf.j2 dest=/etc/apache2/mods-available/pagespeed.conf - notify: - - restart apache2 - -- name: Modules enabled - apache2_module: state=present name="{{ item }}" - with_items: - - proxy_http - - pagespeed - - cache - - proxy_connect - - proxy_html - - rewrite - notify: - - restart apache2 - -- name: VirtualHost configured for the PageSpeed module - template: src=000-default.conf.j2 dest=/etc/apache2/sites-enabled/000-default.conf - notify: - - restart apache2 - -- name: Apache ports configured - template: src=ports.conf.j2 dest=/etc/apache2/ports.conf - notify: - - restart apache2 - -- name: Ensure that the apache2 service directory exist - file: path=/etc/systemd/system/apache2.service.d/ state=directory mode=0755 owner=root group=root - -- name: Setup the cgroup limitations for the apache2 daemon - template: src=apache2_100-CustomLimitations.conf.j2 dest=/etc/systemd/system/apache2.service.d/100-CustomLimitations.conf - notify: - - daemon-reload - - restart apache2 - -- meta: flush_handlers - -- name: Set facts for mobileconfigs - set_fact: - proxy_enabled: true - -- name: Register p12 PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}.p12 | base64 - register: PayloadContent - with_items: "{{ users }}" - -- name: Register CA PayloadContent - shell: > - cat /{{ easyrsa_dir }}/easyrsa3/pki/ca.crt | base64 - register: PayloadContentCA - -- name: Build the mobileconfigs - template: src=roles/vpn/templates/mobileconfig.j2 dest=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item.0 }}_proxy.mobileconfig mode=0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - -- name: Fetch users mobileconfig - fetch: src=/{{ easyrsa_dir }}/easyrsa3//pki/private/{{ item }}_proxy.mobileconfig dest=configs/{{ IP_subject_alt_name }}_{{ item }}_proxy.mobileconfig flat=yes - with_items: "{{ users }}" diff --git a/roles/proxy/templates/000-default.conf.j2 b/roles/proxy/templates/000-default.conf.j2 deleted file mode 100644 index 7aa917b7..00000000 --- a/roles/proxy/templates/000-default.conf.j2 +++ /dev/null @@ -1,11 +0,0 @@ - - - Order deny,allow - Allow from all - - RewriteEngine On - RewriteRule ^(.*)$ http://%{HTTP_HOST}$1 [NC,P] - ProxyPass / http://$1 - ProxyPassReverse / http://$1 - ProxyPreserveHost On - diff --git a/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 b/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 deleted file mode 100644 index 5e9774ef..00000000 --- a/roles/proxy/templates/apache2_100-CustomLimitations.conf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -[Service] -MemoryLimit=134217728 -CPUAccounting=true -CPUQuota=15% diff --git a/roles/proxy/templates/default.filter.j2 b/roles/proxy/templates/default.filter.j2 deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/proxy/templates/pagespeed.conf.j2 b/roles/proxy/templates/pagespeed.conf.j2 deleted file mode 100644 index 026a6864..00000000 --- a/roles/proxy/templates/pagespeed.conf.j2 +++ /dev/null @@ -1,369 +0,0 @@ - - # Turn on mod_pagespeed. To completely disable mod_pagespeed, you - # can set this to "off". - ModPagespeed on - - # We want VHosts to inherit global configuration. - # If this is not included, they'll be independent (except for inherently - # global options), at least for backwards compatibility. - ModPagespeedInheritVHostConfig on - - # Direct Apache to send all HTML output to the mod_pagespeed - # output handler. - AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER text/html - - # If you want mod_pagespeed process XHTML as well, please uncomment this - # line. - # AddOutputFilterByType MOD_PAGESPEED_OUTPUT_FILTER application/xhtml+xml - - # The ModPagespeedFileCachePath directory must exist and be writable - # by the apache user (as specified by the User directive). - ModPagespeedFileCachePath "/var/cache/mod_pagespeed/" - - # LogDir is needed to store various logs, including the statistics log - # required for the console. - ModPagespeedLogDir "/var/log/pagespeed" - - # The locations of SSL Certificates is distribution-dependent. - ModPagespeedSslCertDirectory "/etc/ssl/certs" - - - # If you want, you can use one or more memcached servers as the store for - # the mod_pagespeed cache. - # ModPagespeedMemcachedServers localhost:11211 - - # A portion of the cache can be kept in memory only, to reduce load on disk - # (or memcached) from many small files. - # ModPagespeedCreateSharedMemoryMetadataCache "/var/cache/mod_pagespeed/" 51200 - - # Override the mod_pagespeed 'rewrite level'. The default level - # "CoreFilters" uses a set of rewrite filters that are generally - # safe for most web pages. Most sites should not need to change - # this value and can instead fine-tune the configuration using the - # ModPagespeedDisableFilters and ModPagespeedEnableFilters - # directives, below. Valid values for ModPagespeedRewriteLevel are - # PassThrough, CoreFilters and TestingCoreFilters. - # - ModPagespeedRewriteLevel CoreFilters - - ModPagespeedEnableFilters combine_heads - ModPagespeedEnableFilters combine_javascript - ModPagespeedEnableFilters convert_jpeg_to_webp - ModPagespeedEnableFilters convert_png_to_jpeg - ModPagespeedEnableFilters inline_preview_images - ModPagespeedEnableFilters make_google_analytics_async - ModPagespeedEnableFilters move_css_above_scripts - ModPagespeedEnableFilters move_css_to_head - ModPagespeedEnableFilters resize_mobile_images - ModPagespeedEnableFilters sprite_images - - ModPagespeedEnableFilters defer_iframe - ModPagespeedEnableFilters defer_javascript - ModPagespeedEnableFilters lazyload_images - - # Explicitly disables specific filters. This is useful in - # conjunction with ModPagespeedRewriteLevel. For instance, if one - # of the filters in the CoreFilters needs to be disabled for a - # site, that filter can be added to - # ModPagespeedDisableFilters. This directive contains a - # comma-separated list of filter names, and can be repeated. - # - # ModPagespeedDisableFilters rewrite_images - - # Explicitly enables specific filters. This is useful in - # conjunction with ModPagespeedRewriteLevel. For instance, filters - # not included in the CoreFilters may be enabled using this - # directive. This directive contains a comma-separated list of - # filter names, and can be repeated. - # - # ModPagespeedEnableFilters rewrite_javascript,rewrite_css - # ModPagespeedEnableFilters collapse_whitespace,elide_attributes - - # Explicitly forbids the enabling of specific filters using either query - # parameters or request headers. This is useful, for example, when we do - # not want the filter to run for performance or security reasons. This - # directive contains a comma-separated list of filter names, and can be - # repeated. - # - # ModPagespeedForbidFilters rewrite_images - - # How long mod_pagespeed will wait to return an optimized resource - # (per flush window) on first request before giving up and returning the - # original (unoptimized) resource. After this deadline is exceeded the - # original resource is returned and the optimization is pushed to the - # background to be completed for future requests. Increasing this value will - # increase page latency, but might reduce load time (for instance on a - # bandwidth-constrained link where it's worth waiting for image - # compression to complete). If the value is less than or equal to zero - # mod_pagespeed will wait indefinitely for the rewrite to complete before - # returning. - # - # ModPagespeedRewriteDeadlinePerFlushMs 10 - - # ModPagespeedDomain - # authorizes rewriting of JS, CSS, and Image files found in this - # domain. By default only resources with the same origin as the - # HTML file are rewritten. For example: - # - ModPagespeedDomain * - # - # This will allow resources found on http://cdn.myhost.com to be - # rewritten in addition to those in the same domain as the HTML. - # - # Other domain-related directives (like ModPagespeedMapRewriteDomain - # and ModPagespeedMapOriginDomain) can also authorize domains. - # - # Wildcards (* and ?) are allowed in the domain specification. Be - # careful when using them as if you rewrite domains that do not - # send you traffic, then the site receiving the traffic will not - # know how to serve the rewritten content. - - # If you use downstream caches such as varnish or proxy_cache for caching - # HTML, you can configure pagespeed to work with these caches correctly - # using the following directives. Note that the values for - # ModPagespeedDownstreamCachePurgeLocationPrefix and - # ModPagespeedDownstreamCacheRebeaconingKey are deliberately left empty here - # in order to force the webmaster to choose appropriate value for these. - # - # ModPagespeedDownstreamCachePurgeLocationPrefix - # ModPagespeedDownstreamCachePurgeMethod PURGE - # ModPagespeedDownstreamCacheRewrittenPercentageThreshold 95 - # ModPagespeedDownstreamCacheRebeaconingKey - - # Other defaults (cache sizes and thresholds): - # - # ModPagespeedFileCacheSizeKb 102400 - # ModPagespeedFileCacheCleanIntervalMs 3600000 - # ModPagespeedLRUCacheKbPerProcess 1024 - # ModPagespeedLRUCacheByteLimit 16384 - # ModPagespeedCssFlattenMaxBytes 102400 - # ModPagespeedCssInlineMaxBytes 2048 - # ModPagespeedCssImageInlineMaxBytes 0 - # ModPagespeedImageInlineMaxBytes 3072 - # ModPagespeedJsInlineMaxBytes 2048 - # ModPagespeedCssOutlineMinBytes 3000 - # ModPagespeedJsOutlineMinBytes 3000 - # ModPagespeedMaxCombinedCssBytes -1 - # ModPagespeedMaxCombinedJsBytes 92160 - - # Limit the number of inodes in the file cache. Set to 0 for no limit. - # The default value if this parameter is not specified is 0 (no limit). - ModPagespeedFileCacheInodeLimit 500000 - - # Bound the number of images that can be rewritten at any one time; this - # avoids overloading the CPU. Set this to 0 to remove the bound. - # - # ModPagespeedImageMaxRewritesAtOnce 8 - - # You can also customize the number of threads per Apache process - # mod_pagespeed will use to do resource optimization. Plain - # "rewrite threads" are used to do short, latency-sensitive work, - # while "expensive rewrite threads" are used for actual optimization - # work that's more computationally expensive. If you live these unset, - # or use values <= 0 the defaults will be used, which is 1 for both - # values when using non-threaded MPMs (e.g. prefork) and 4 for both - # on threaded MPMs (e.g. worker and event). These settings can only - # be changed globally, and not per virtual host. - # - # ModPagespeedNumRewriteThreads 4 - # ModPagespeedNumExpensiveRewriteThreads 4 - - # Randomly drop rewrites (*) to increase the chance of optimizing - # frequently fetched resources and decrease the chance of optimizing - # infrequently fetched resources. This can reduce CPU load. The default - # value of this parameter is 0 (no drops). 90 means that a resourced - # fetched once has a 10% probability of being optimized while a resource - # that is fetched 50 times has a 99.65% probability of being optimized. - # - # (*) Currently only CSS files and images are randomly dropped. Images - # within CSS files are not randomly dropped. - # - # ModPagespeedRewriteRandomDropPercentage 90 - - # Many filters modify the URLs of resources in HTML files. This is typically - # harmless but pages whose JavaScript expects to read or modify the original - # URLs may break. The following parameters prevent filters from modifying - # URLs of their respective types. - # - # ModPagespeedJsPreserveURLs on - # ModPagespeedImagePreserveURLs on - # ModPagespeedCssPreserveURLs on - - # When PreserveURLs is on, it is still possible to enable browser-specific - # optimizations (for example, webp images can be served to browsers that - # will accept them). They'll be served with Vary: Accept or Vary: - # User-Agent headers as appropriate. Note that this may require configuring - # reverse proxy caches such as varnish to handle these headers properly. - # - # ModPagespeedFilters in_place_optimize_for_browser - - # Internet Explorer has difficulty caching resources with Vary: headers. - # They will either be uncached (older IE) or require revalidation. See: - # http://blogs.msdn.com/b/ieinternals/archive/2009/06/17/vary-header-prevents-caching-in-ie.aspx - # As a result we serve them as Cache-Control: private instead by default. - # If you are using a reverse proxy or CDN configured to cache content with - # the Vary: Accept header you should turn this setting off. - # - # ModPagespeedPrivateNotVaryForIE on - - # Settings for image optimization: - # - # Lossy image recompression quality (0 to 100, -1 just strips metadata): - # ModPagespeedImageRecompressionQuality 85 - # - # Jpeg recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedJpegRecompressionQuality -1 - # ModPagespeedJpegRecompressionQualityForSmallScreens 70 - - ModPagespeedJpegRecompressionQuality 75 - - # - # WebP recompression quality (0 to 100, -1 uses ImageRecompressionQuality): - # ModPagespeedWebpRecompressionQuality 80 - # ModPagespeedWebpRecompressionQualityForSmallScreens 70 - # - # Timeout for conversions to WebP format, in - # milliseconds. Negative values mean no timeout is applied. The - # default value is -1: - # ModPagespeedWebpTimeoutMs 5000 - # - # Percent of original image size below which optimized images are retained: - # ModPagespeedImageLimitOptimizedPercent 100 - # - # Percent of original image area below which image resizing will be - # attempted: - # ModPagespeedImageLimitResizeAreaPercent 100 - - # Settings for inline preview images - # - # Setting this to n restricts preview images to the first n images found on - # the page. The default of -1 means preview images can appear anywhere on - # the page (if those images appear above the fold). - # ModPagespeedMaxInlinedPreviewImagesIndex -1 - - # Sets the minimum size in bytes of any image for which a low quality image - # is generated. - # ModPagespeedMinImageSizeLowResolutionBytes 3072 - - # The maximum URL size is generally limited to about 2k characters - # due to IE: See http://support.microsoft.com/kb/208427/EN-US. - # Apache servers by default impose a further limitation of about - # 250 characters per URL segment (text between slashes). - # mod_pagespeed circumvents this limitation, but if you employ - # proxy servers in your path you may need to re-impose it by - # overriding the setting here. The default setting is 1024 - # characters. - # - # ModPagespeedMaxSegmentLength 250 - - # Uncomment this if you want to prevent mod_pagespeed from combining files - # (e.g. CSS files) across paths - # - # ModPagespeedCombineAcrossPaths off - - # Renaming JavaScript URLs can sometimes break them. With this - # option enabled, mod_pagespeed uses a simple heuristic to decide - # not to rename JavaScript that it thinks is introspective. - # - # You can uncomment this to let mod_pagespeed rename all JS files. - # - # ModPagespeedAvoidRenamingIntrospectiveJavaScript off - - # Certain common JavaScript libraries are available from Google, which acts - # as a CDN and allows you to benefit from browser caching if a new visitor - # to your site previously visited another site that makes use of the same - # libraries as you do. Enable the following filter to turn on this feature. - # - # ModPagespeedEnableFilters canonicalize_javascript_libraries - - # The following line configures a library that is recognized by - # canonicalize_javascript_libraries. This will have no effect unless you - # enable this filter (generally by uncommenting the last line in the - # previous stanza). The format is: - # ModPagespeedLibrary bytes md5 canonical_url - # Where bytes and md5 are with respect to the *minified* JS; use - # js_minify --print_size_and_hash to obtain this data. - # Note that we can register multiple hashes for the same canonical url; - # we do this if there are versions available that have already been minified - # with more sophisticated tools. - # - # Additional library configuration can be found in - # pagespeed_libraries.conf included in the distribution. You should add - # new entries here, though, so that file can be automatically upgraded. - # ModPagespeedLibrary 43 1o978_K0_LNE5_ystNklf http://www.modpagespeed.com/rewrite_javascript.js - - # Explicitly tell mod_pagespeed to load some resources from disk. - # This will speed up load time and update frequency. - # - # This should only be used for static resources which do not need - # specific headers set or other processing by Apache. - # - # Both URL and filesystem path should specify directories and - # filesystem path must be absolute (for now). - # - # ModPagespeedLoadFromFile "http://example.com/static/" "/var/www/static/" - - - # Enables server-side instrumentation and statistics. If this rewriter is - # enabled, then each rewritten HTML page will have instrumentation javascript - # added that sends latency beacons to /mod_pagespeed_beacon. These - # statistics can be accessed at /mod_pagespeed_statistics. You must also - # enable the mod_pagespeed_statistics and mod_pagespeed_beacon handlers - # below. - # - # ModPagespeedEnableFilters add_instrumentation - - # The add_instrumentation filter sends a beacon after the page onload - # handler is called. The user might navigate to a new URL before this. If - # you enable the following directive, the beacon is sent as part of an - # onbeforeunload handler, for pages where navigation happens before the - # onload event. - # - # ModPagespeedReportUnloadTime on - - # Uncomment the following line so that ModPagespeed will not cache or - # rewrite resources with Vary: in the header, e.g. Vary: User-Agent. - # Note that ModPagespeed always respects Vary: headers on html content. - # ModPagespeedRespectVary on - - # Uncomment the following line if you want to disable statistics entirely. - # - # ModPagespeedStatistics off - - # These handlers are central entry-points into the admin pages. - # By default, pagespeed_admin and pagespeed_global_admin present - # the same data, and differ only when - # ModPagespeedUsePerVHostStatistics is enabled. In that case, - # /pagespeed_global_admin sees aggregated data across all vhosts, - # and the /pagespeed_admin sees data only for a particular vhost. - # - # You may insert other "Allow from" lines to add hosts you want to - # allow to look at generated statistics. Another possibility is - # to comment out the "Order" and "Allow" options from the config - # file, to allow any client that can reach your server to access - # and change server state, such as statistics, caches, and - # messages. This might be appropriate in an experimental setup. - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_admin - - - Order allow,deny - Allow from localhost - Allow from 127.0.0.1 - SetHandler pagespeed_global_admin - - - # Enable logging of mod_pagespeed statistics, needed for the console. - ModPagespeedStatisticsLogging on - - # Page /mod_pagespeed_message lets you view the latest messages from - # mod_pagespeed, regardless of log-level in your httpd.conf - # ModPagespeedMessageBufferSize is the maximum number of bytes you would - # like to dump to your /mod_pagespeed_message page at one time, - # its default value is 100k bytes. - # Set it to 0 if you want to disable this feature. - ModPagespeedMessageBufferSize 100000 - diff --git a/roles/proxy/templates/ports.conf.j2 b/roles/proxy/templates/ports.conf.j2 deleted file mode 100644 index eb6be226..00000000 --- a/roles/proxy/templates/ports.conf.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# If you just change the port or add more ports here, you will likely also -# have to change the VirtualHost statement in -# /etc/apache2/sites-enabled/000-default.conf - -Listen {{ local_service_ip }}:8080 - - - Listen {{ local_service_ip }}:443 - - - - Listen {{ local_service_ip }}:443 - diff --git a/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 b/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 deleted file mode 100644 index cd9b628c..00000000 --- a/roles/proxy/templates/privoxy_100-CustomLimitations.conf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -[Service] -MemoryLimit=33554432 -CPUAccounting=true -CPUQuota=15% diff --git a/roles/proxy/templates/privoxy_config.j2 b/roles/proxy/templates/privoxy_config.j2 deleted file mode 100644 index dcbe3484..00000000 --- a/roles/proxy/templates/privoxy_config.j2 +++ /dev/null @@ -1,2107 +0,0 @@ -# Sample Configuration File for Privoxy -# -# Id: config,v -# -# Copyright (C) 2001-2014 Privoxy Developers http://www.privoxy.org/ -# -#################################################################### -# # -# Table of Contents # -# # -# I. INTRODUCTION # -# II. FORMAT OF THE CONFIGURATION FILE # -# # -# 1. LOCAL SET-UP DOCUMENTATION # -# 2. CONFIGURATION AND LOG FILE LOCATIONS # -# 3. DEBUGGING # -# 4. ACCESS CONTROL AND SECURITY # -# 5. FORWARDING # -# 6. MISCELLANEOUS # -# 7. WINDOWS GUI OPTIONS # -# # -#################################################################### -# -# -# I. INTRODUCTION -# =============== -# -# This file holds Privoxy's main configuration. Privoxy detects -# configuration changes automatically, so you don't have to restart -# it unless you want to load a different configuration file. -# -# The configuration will be reloaded with the first request after -# the change was done, this request itself will still use the old -# configuration, though. In other words: it takes two requests -# before you see the result of your changes. Requests that are -# dropped due to ACL don't trigger reloads. -# -# When starting Privoxy on Unix systems, give the location of this -# file as last argument. On Windows systems, Privoxy will look for -# this file with the name 'config.txt' in the current working -# directory of the Privoxy process. -# -# -# II. FORMAT OF THE CONFIGURATION FILE -# ==================================== -# -# Configuration lines consist of an initial keyword followed by a -# list of values, all separated by whitespace (any number of spaces -# or tabs). For example, -# -# actionsfile default.action -# -# Indicates that the actionsfile is named 'default.action'. -# -# The '#' indicates a comment. Any part of a line following a '#' is -# ignored, except if the '#' is preceded by a '\'. -# -# Thus, by placing a # at the start of an existing configuration -# line, you can make it a comment and it will be treated as if it -# weren't there. This is called "commenting out" an option and can -# be useful. Removing the # again is called "uncommenting". -# -# Note that commenting out an option and leaving it at its default -# are two completely different things! Most options behave very -# differently when unset. See the "Effect if unset" explanation in -# each option's description for details. -# -# Long lines can be continued on the next line by using a `\' as the -# last character. -# -# -# 1. LOCAL SET-UP DOCUMENTATION -# ============================== -# -# If you intend to operate Privoxy for more users than just -# yourself, it might be a good idea to let them know how to reach -# you, what you block and why you do that, your policies, etc. -# -# -# 1.1. user-manual -# ================= -# -# Specifies: -# -# Location of the Privoxy User Manual. -# -# Type of value: -# -# A fully qualified URI -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# http://www.privoxy.org/version/user-manual/ will be used, -# where version is the Privoxy version. -# -# Notes: -# -# The User Manual URI is the single best source of information -# on Privoxy, and is used for help links from some of the -# internal CGI pages. The manual itself is normally packaged -# with the binary distributions, so you probably want to set -# this to a locally installed copy. -# -# Examples: -# -# The best all purpose solution is simply to put the full local -# PATH to where the User Manual is located: -# -# user-manual /usr/share/doc/privoxy/user-manual -# -# The User Manual is then available to anyone with access to -# Privoxy, by following the built-in URL: http:// -# config.privoxy.org/user-manual/ (or the shortcut: http://p.p/ -# user-manual/). -# -# If the documentation is not on the local system, it can be -# accessed from a remote server, as: -# -# user-manual http://example.com/privoxy/user-manual/ -# -# WARNING!!! -# -# If set, this option should be the first option in the -# config file, because it is used while the config file is -# being read. -# -user-manual /usr/share/doc/privoxy/user-manual -# -# 1.2. trust-info-url -# ==================== -# -# Specifies: -# -# A URL to be displayed in the error page that users will see if -# access to an untrusted page is denied. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No links are displayed on the "untrusted" error page. -# -# Notes: -# -# The value of this option only matters if the experimental -# trust mechanism has been activated. (See trustfile below.) -# -# If you use the trust mechanism, it is a good idea to write up -# some on-line documentation about your trust policy and to -# specify the URL(s) here. Use multiple times for multiple URLs. -# -# The URL(s) should be added to the trustfile as well, so users -# don't end up locked out from the information on why they were -# locked out in the first place! -# -#trust-info-url http://www.example.com/why_we_block.html -#trust-info-url http://www.example.com/what_we_allow.html -# -# 1.3. admin-address -# =================== -# -# Specifies: -# -# An email address to reach the Privoxy administrator. -# -# Type of value: -# -# Email address -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No email address is displayed on error pages and the CGI user -# interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -#admin-address privoxy-admin@example.com -# -# 1.4. proxy-info-url -# ==================== -# -# Specifies: -# -# A URL to documentation about the local Privoxy setup, -# configuration or policies. -# -# Type of value: -# -# URL -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# No link to local documentation is displayed on error pages and -# the CGI user interface. -# -# Notes: -# -# If both admin-address and proxy-info-url are unset, the whole -# "Local Privoxy Support" box on all generated pages will not be -# shown. -# -# This URL shouldn't be blocked ;-) -# -#proxy-info-url http://www.example.com/proxy-service.html -# -# 2. CONFIGURATION AND LOG FILE LOCATIONS -# ======================================== -# -# Privoxy can (and normally does) use a number of other files for -# additional configuration, help and logging. This section of the -# configuration file tells Privoxy where to find those other files. -# -# The user running Privoxy, must have read permission for all -# configuration files, and write permission to any files that would -# be modified, such as log files and actions files. -# -# -# 2.1. confdir -# ============= -# -# Specifies: -# -# The directory where the other configuration files are located. -# -# Type of value: -# -# Path name -# -# Default value: -# -# /etc/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -confdir /etc/privoxy -# -# 2.2. templdir -# ============== -# -# Specifies: -# -# An alternative directory where the templates are loaded from. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# The templates are assumed to be located in confdir/template. -# -# Notes: -# -# Privoxy's original templates are usually overwritten with each -# update. Use this option to relocate customized templates that -# should be kept. As template variables might change between -# updates, you shouldn't expect templates to work with Privoxy -# releases other than the one they were part of, though. -# -#templdir . -# -# 2.3. temporary-directory -# ========================= -# -# Specifies: -# -# A directory where Privoxy can create temporary files. -# -# Type of value: -# -# Path name -# -# Default value: -# -# unset -# -# Effect if unset: -# -# No temporary files are created, external filters don't work. -# -# Notes: -# -# To execute external filters, Privoxy has to create temporary -# files. This directive specifies the directory the temporary -# files should be written to. -# -# It should be a directory only Privoxy (and trusted users) can -# access. -# -#temporary-directory . -# -# 2.4. logdir -# ============ -# -# Specifies: -# -# The directory where all logging takes place (i.e. where the -# logfile is located). -# -# Type of value: -# -# Path name -# -# Default value: -# -# /var/log/privoxy (Unix) or Privoxy installation dir (Windows) -# -# Effect if unset: -# -# Mandatory -# -# Notes: -# -# No trailing "/", please. -# -logdir /var/log/privoxy -# -# 2.5. actionsfile -# ================= -# -# Specifies: -# -# The actions file(s) to use -# -# Type of value: -# -# Complete file name, relative to confdir -# -# Default values: -# -# match-all.action # Actions that are applied to all sites and maybe overruled later on. -# -# default.action # Main actions file -# -# user.action # User customizations -# -# Effect if unset: -# -# No actions are taken at all. More or less neutral proxying. -# -# Notes: -# -# Multiple actionsfile lines are permitted, and are in fact -# recommended! -# -# The default values are default.action, which is the "main" -# actions file maintained by the developers, and user.action, -# where you can make your personal additions. -# -# Actions files contain all the per site and per URL -# configuration for ad blocking, cookie management, privacy -# considerations, etc. -# -actionsfile match-all.action # Actions that are applied to all sites and maybe overruled later on. -actionsfile default.action # Main actions file -actionsfile user.action # User customizations -# -# 2.6. filterfile -# ================ -# -# Specifies: -# -# The filter file(s) to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# default.filter (Unix) or default.filter.txt (Windows) -# -# Effect if unset: -# -# No textual content filtering takes place, i.e. all +filter{name} -# actions in the actions files are turned neutral. -# -# Notes: -# -# Multiple filterfile lines are permitted. -# -# The filter files contain content modification rules that use -# regular expressions. These rules permit powerful changes on -# the content of Web pages, and optionally the headers as well, -# e.g., you could try to disable your favorite JavaScript -# annoyances, re-write the actual displayed text, or just have -# some fun playing buzzword bingo with web pages. -# -# The +filter{name} actions rely on the relevant filter (name) -# to be defined in a filter file! -# -# A pre-defined filter file called default.filter that contains -# a number of useful filters for common problems is included in -# the distribution. See the section on the filter action for a -# list. -# -# It is recommended to place any locally adapted filters into a -# separate file, such as user.filter. -# -filterfile default.filter -filterfile user.filter # User customizations -# -# 2.7. logfile -# ============= -# -# Specifies: -# -# The log file to use -# -# Type of value: -# -# File name, relative to logdir -# -# Default value: -# -# Unset (commented out). When activated: logfile (Unix) or -# privoxy.log (Windows). -# -# Effect if unset: -# -# No logfile is written. -# -# Notes: -# -# The logfile is where all logging and error messages are -# written. The level of detail and number of messages are set -# with the debug option (see below). The logfile can be useful -# for tracking down a problem with Privoxy (e.g., it's not -# blocking an ad you think it should block) and it can help you -# to monitor what your browser is doing. -# -# Depending on the debug options below, the logfile may be a -# privacy risk if third parties can get access to it. As most -# users will never look at it, Privoxy only logs fatal errors by -# default. -# -# For most troubleshooting purposes, you will have to change -# that, please refer to the debugging section for details. -# -# Any log files must be writable by whatever user Privoxy is -# being run as (on Unix, default user id is "privoxy"). -# -# To prevent the logfile from growing indefinitely, it is -# recommended to periodically rotate or shorten it. Many -# operating systems support log rotation out of the box, some -# require additional software to do it. For details, please -# refer to the documentation for your operating system. -# -logfile logfile -# -# 2.8. trustfile -# =============== -# -# Specifies: -# -# The name of the trust file to use -# -# Type of value: -# -# File name, relative to confdir -# -# Default value: -# -# Unset (commented out). When activated: trust (Unix) or -# trust.txt (Windows) -# -# Effect if unset: -# -# The entire trust mechanism is disabled. -# -# Notes: -# -# The trust mechanism is an experimental feature for building -# white-lists and should be used with care. It is NOT -# recommended for the casual user. -# -# If you specify a trust file, Privoxy will only allow access to -# sites that are specified in the trustfile. Sites can be listed -# in one of two ways: -# -# Prepending a ~ character limits access to this site only (and -# any sub-paths within this site), e.g. ~www.example.com allows -# access to ~www.example.com/features/news.html, etc. -# -# Or, you can designate sites as trusted referrers, by -# prepending the name with a + character. The effect is that -# access to untrusted sites will be granted -- but only if a -# link from this trusted referrer was used to get there. The -# link target will then be added to the "trustfile" so that -# future, direct accesses will be granted. Sites added via this -# mechanism do not become trusted referrers themselves (i.e. -# they are added with a ~ designation). There is a limit of 512 -# such entries, after which new entries will not be made. -# -# If you use the + operator in the trust file, it may grow -# considerably over time. -# -# It is recommended that Privoxy be compiled with the -# --disable-force, --disable-toggle and --disable-editor -# options, if this feature is to be used. -# -# Possible applications include limiting Internet access for -# children. -# -#trustfile trust -# -# 3. DEBUGGING -# ============= -# -# These options are mainly useful when tracing a problem. Note that -# you might also want to invoke Privoxy with the --no-daemon command -# line option when debugging. -# -# -# 3.1. debug -# =========== -# -# Specifies: -# -# Key values that determine what information gets logged. -# -# Type of value: -# -# Integer values -# -# Default value: -# -# 0 (i.e.: only fatal errors (that cause Privoxy to exit) are -# logged) -# -# Effect if unset: -# -# Default value is used (see above). -# -# Notes: -# -# The available debug levels are: -# -# debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -# debug 2 # show each connection status -# debug 4 # show I/O status -# debug 8 # show header parsing -# debug 16 # log all data written to the network -# debug 32 # debug force feature -# debug 64 # debug regular expression filters -# debug 128 # debug redirects -# debug 256 # debug GIF de-animation -# debug 512 # Common Log Format -# debug 1024 # Log the destination for requests Privoxy didn't let through, and the reason why. -# debug 2048 # CGI user interface -# debug 4096 # Startup banner and warnings. -# debug 8192 # Non-fatal errors -# debug 32768 # log all data read from the network -# debug 65536 # Log the applying actions -# -# To select multiple debug levels, you can either add them or -# use multiple debug lines. -# -# A debug level of 1 is informative because it will show you -# each request as it happens. 1, 1024, 4096 and 8192 are -# recommended so that you will notice when things go wrong. The -# other levels are probably only of interest if you are hunting -# down a specific problem. They can produce a hell of an output -# (especially 16). -# -# If you are used to the more verbose settings, simply enable -# the debug lines below again. -# -# If you want to use pure CLF (Common Log Format), you should -# set "debug 512" ONLY and not enable anything else. -# -# Privoxy has a hard-coded limit for the length of log messages. -# If it's reached, messages are logged truncated and marked with -# "... [too long, truncated]". -# -# Please don't file any support requests without trying to -# reproduce the problem with increased debug level first. Once -# you read the log messages, you may even be able to solve the -# problem on your own. -# -#debug 1 # Log the destination for each request Privoxy let through. See also debug 1024. -#debug 1024 # Actions that are applied to all sites and maybe overruled later on. -#debug 4096 # Startup banner and warnings -#debug 8192 # Non-fatal errors -# -# 3.2. single-threaded -# ===================== -# -# Specifies: -# -# Whether to run only one server thread. -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Multi-threaded (or, where unavailable: forked) operation, i.e. -# the ability to serve multiple requests simultaneously. -# -# Notes: -# -# This option is only there for debugging purposes. It will -# drastically reduce performance. -# -#single-threaded 1 -# -# 3.3. hostname -# ============== -# -# Specifies: -# -# The hostname shown on the CGI pages. -# -# Type of value: -# -# Text -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# The hostname provided by the operating system is used. -# -# Notes: -# -# On some misconfigured systems resolving the hostname fails or -# takes too much time and slows Privoxy down. Setting a fixed -# hostname works around the problem. -# -# In other circumstances it might be desirable to show a -# hostname other than the one returned by the operating system. -# For example if the system has several different hostnames and -# you don't want to use the first one. -# -# Note that Privoxy does not validate the specified hostname -# value. -# -#hostname hostname.example.org -# -# 4. ACCESS CONTROL AND SECURITY -# =============================== -# -# This section of the config file controls the security-relevant -# aspects of Privoxy's configuration. -# -# -# 4.1. listen-address -# ==================== -# -# Specifies: -# -# The address and TCP port on which Privoxy will listen for -# client requests. -# -# Type of value: -# -# [IP-Address]:Port -# -# [Hostname]:Port -# -# Default value: -# -# 127.0.0.1:8118 -# -# Effect if unset: -# -# Bind to 127.0.0.1 (IPv4 localhost), port 8118. This is -# suitable and recommended for home users who run Privoxy on the -# same machine as their browser. -# -# Notes: -# -# You will need to configure your browser(s) to this proxy -# address and port. -# -# If you already have another service running on port 8118, or -# if you want to serve requests from other machines (e.g. on -# your local network) as well, you will need to override the -# default. -# -# You can use this statement multiple times to make Privoxy -# listen on more ports or more IP addresses. Suitable if your -# operating system does not support sharing IPv6 and IPv4 -# protocols on the same socket. -# -# If a hostname is used instead of an IP address, Privoxy will -# try to resolve it to an IP address and if there are multiple, -# use the first one returned. -# -# If the address for the hostname isn't already known on the -# system (for example because it's in /etc/hostname), this may -# result in DNS traffic. -# -# If the specified address isn't available on the system, or if -# the hostname can't be resolved, Privoxy will fail to start. -# -# IPv6 addresses containing colons have to be quoted by -# brackets. They can only be used if Privoxy has been compiled -# with IPv6 support. If you aren't sure if your version supports -# it, have a look at http://config.privoxy.org/show-status. -# -# Some operating systems will prefer IPv6 to IPv4 addresses even -# if the system has no IPv6 connectivity which is usually not -# expected by the user. Some even rely on DNS to resolve -# localhost which mean the "localhost" address used may not -# actually be local. -# -# It is therefore recommended to explicitly configure the -# intended IP address instead of relying on the operating -# system, unless there's a strong reason not to. -# -# If you leave out the address, Privoxy will bind to all IPv4 -# interfaces (addresses) on your machine and may become -# reachable from the Internet and/or the local network. Be aware -# that some GNU/Linux distributions modify that behaviour -# without updating the documentation. Check for non-standard -# patches if your Privoxy version behaves differently. -# -# If you configure Privoxy to be reachable from the network, -# consider using access control lists (ACL's, see below), and/or -# a firewall. -# -# If you open Privoxy to untrusted users, you will also want to -# make sure that the following actions are disabled: -# enable-edit-actions and enable-remote-toggle -# -# Example: -# -# Suppose you are running Privoxy on a machine which has the -# address 192.168.0.1 on your local private network -# (192.168.0.0) and has another outside connection with a -# different address. You want it to serve requests from inside -# only: -# -# listen-address 192.168.0.1:8118 -# -# Suppose you are running Privoxy on an IPv6-capable machine and -# you want it to listen on the IPv6 address of the loopback -# device: -# -# listen-address [::1]:8118 -# -# -listen-address {{ local_service_ip }}:8118 -# -# 4.2. toggle -# ============ -# -# Specifies: -# -# Initial state of "toggle" status -# -# Type of value: -# -# 1 or 0 -# -# Default value: -# -# 1 -# -# Effect if unset: -# -# Act as if toggled on -# -# Notes: -# -# If set to 0, Privoxy will start in "toggled off" mode, i.e. -# mostly behave like a normal, content-neutral proxy with both -# ad blocking and content filtering disabled. See -# enable-remote-toggle below. -# -toggle 1 -# -# 4.3. enable-remote-toggle -# ========================== -# -# Specifies: -# -# Whether or not the web-based toggle feature may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based toggle feature is disabled. -# -# Notes: -# -# When toggled off, Privoxy mostly acts like a normal, -# content-neutral proxy, i.e. doesn't block ads or filter -# content. -# -# Access to the toggle feature can not be controlled separately -# by "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# toggle it for all users. So this option is not recommended for -# multi-user environments with untrusted users. -# -# Note that malicious client side code (e.g Java) is also -# capable of using this option. -# -# As a lot of Privoxy users don't read documentation, this -# feature is disabled by default. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-remote-toggle 0 -# -# 4.4. enable-remote-http-toggle -# =============================== -# -# Specifies: -# -# Whether or not Privoxy recognizes special HTTP headers to -# change its behaviour. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores special HTTP headers. -# -# Notes: -# -# When toggled on, the client can change Privoxy's behaviour by -# setting special HTTP headers. Currently the only supported -# special header is "X-Filter: No", to disable filtering for the -# ongoing request, even if it is enabled in one of the action -# files. -# -# This feature is disabled by default. If you are using Privoxy -# in a environment with trusted clients, you may enable this -# feature at your discretion. Note that malicious client side -# code (e.g Java) is also capable of using this feature. -# -# This option will be removed in future releases as it has been -# obsoleted by the more general header taggers. -# -enable-remote-http-toggle 0 -# -# 4.5. enable-edit-actions -# ========================= -# -# Specifies: -# -# Whether or not the web-based actions file editor may be used -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The web-based actions file editor is disabled. -# -# Notes: -# -# Access to the editor can not be controlled separately by -# "ACLs" or HTTP authentication, so that everybody who can -# access Privoxy (see "ACLs" and listen-address above) can -# modify its configuration for all users. -# -# This option is not recommended for environments with untrusted -# users and as a lot of Privoxy users don't read documentation, -# this feature is disabled by default. -# -# Note that malicious client side code (e.g Java) is also -# capable of using the actions editor and you shouldn't enable -# this options unless you understand the consequences and are -# sure your browser is configured correctly. -# -# Note that you must have compiled Privoxy with support for this -# feature, otherwise this option has no effect. -# -enable-edit-actions 0 -# -# 4.6. enforce-blocks -# ==================== -# -# Specifies: -# -# Whether the user is allowed to ignore blocks and can "go there -# anyway". -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Blocks are not enforced. -# -# Notes: -# -# Privoxy is mainly used to block and filter requests as a -# service to the user, for example to block ads and other junk -# that clogs the pipes. Privoxy's configuration isn't perfect -# and sometimes innocent pages are blocked. In this situation it -# makes sense to allow the user to enforce the request and have -# Privoxy ignore the block. -# -# In the default configuration Privoxy's "Blocked" page contains -# a "go there anyway" link to adds a special string (the force -# prefix) to the request URL. If that link is used, Privoxy will -# detect the force prefix, remove it again and let the request -# pass. -# -# Of course Privoxy can also be used to enforce a network -# policy. In that case the user obviously should not be able to -# bypass any blocks, and that's what the "enforce-blocks" option -# is for. If it's enabled, Privoxy hides the "go there anyway" -# link. If the user adds the force prefix by hand, it will not -# be accepted and the circumvention attempt is logged. -# -# Examples: -# -# enforce-blocks 1 -# -enforce-blocks 0 -# -# 4.7. ACLs: permit-access and deny-access -# ========================================= -# -# Specifies: -# -# Who can access what. -# -# Type of value: -# -# src_addr[:port][/src_masklen] [dst_addr[:port][/dst_masklen]] -# -# Where src_addr and dst_addr are IPv4 addresses in dotted -# decimal notation or valid DNS names, port is a port number, -# and src_masklen and dst_masklen are subnet masks in CIDR -# notation, i.e. integer values from 2 to 30 representing the -# length (in bits) of the network address. The masks and the -# whole destination part are optional. -# -# If your system implements RFC 3493, then src_addr and dst_addr -# can be IPv6 addresses delimited by brackets, port can be a -# number or a service name, and src_masklen and dst_masklen can -# be a number from 0 to 128. -# -# Default value: -# -# Unset -# -# If no port is specified, any port will match. If no -# src_masklen or src_masklen is given, the complete IP address -# has to match (i.e. 32 bits for IPv4 and 128 bits for IPv6). -# -# Effect if unset: -# -# Don't restrict access further than implied by listen-address -# -# Notes: -# -# Access controls are included at the request of ISPs and -# systems administrators, and are not usually needed by -# individual users. For a typical home user, it will normally -# suffice to ensure that Privoxy only listens on the localhost -# (127.0.0.1) or internal (home) network address by means of the -# listen-address option. -# -# Please see the warnings in the FAQ that Privoxy is not -# intended to be a substitute for a firewall or to encourage -# anyone to defer addressing basic security weaknesses. -# -# Multiple ACL lines are OK. If any ACLs are specified, Privoxy -# only talks to IP addresses that match at least one -# permit-access line and don't match any subsequent deny-access -# line. In other words, the last match wins, with the default -# being deny-access. -# -# If Privoxy is using a forwarder (see forward below) for a -# particular destination URL, the dst_addr that is examined is -# the address of the forwarder and NOT the address of the -# ultimate target. This is necessary because it may be -# impossible for the local Privoxy to determine the IP address -# of the ultimate target (that's often what gateways are used -# for). -# -# You should prefer using IP addresses over DNS names, because -# the address lookups take time. All DNS names must resolve! You -# can not use domain patterns like "*.org" or partial domain -# names. If a DNS name resolves to multiple IP addresses, only -# the first one is used. -# -# Some systems allow IPv4 clients to connect to IPv6 server -# sockets. Then the client's IPv4 address will be translated by -# the system into IPv6 address space with special prefix -# ::ffff:0:0/96 (so called IPv4 mapped IPv6 address). Privoxy -# can handle it and maps such ACL addresses automatically. -# -# Denying access to particular sites by ACL may have undesired -# side effects if the site in question is hosted on a machine -# which also hosts other sites (most sites are). -# -# Examples: -# -# Explicitly define the default behavior if no ACL and -# listen-address are set: "localhost" is OK. The absence of a -# dst_addr implies that all destination addresses are OK: -# -# permit-access localhost -# -# Allow any host on the same class C subnet as www.privoxy.org -# access to nothing but www.example.com (or other domains hosted -# on the same system): -# -# permit-access www.privoxy.org/24 www.example.com/32 -# -# Allow access from any host on the 26-bit subnet 192.168.45.64 -# to anywhere, with the exception that 192.168.45.73 may not -# access the IP address behind www.dirty-stuff.example.com: -# -# permit-access 192.168.45.64/26 -# deny-access 192.168.45.73 www.dirty-stuff.example.com -# -# Allow access from the IPv4 network 192.0.2.0/24 even if -# listening on an IPv6 wild card address (not supported on all -# platforms): -# -# permit-access 192.0.2.0/24 -# -# This is equivalent to the following line even if listening on -# an IPv4 address (not supported on all platforms): -# -# permit-access [::ffff:192.0.2.0]/120 -# -# -# 4.8. buffer-limit -# ================== -# -# Specifies: -# -# Maximum size of the buffer for content filtering. -# -# Type of value: -# -# Size in Kbytes -# -# Default value: -# -# 4096 -# -# Effect if unset: -# -# Use a 4MB (4096 KB) limit. -# -# Notes: -# -# For content filtering, i.e. the +filter and +deanimate-gif -# actions, it is necessary that Privoxy buffers the entire -# document body. This can be potentially dangerous, since a -# server could just keep sending data indefinitely and wait for -# your RAM to exhaust -- with nasty consequences. Hence this -# option. -# -# When a document buffer size reaches the buffer-limit, it is -# flushed to the client unfiltered and no further attempt to -# filter the rest of the document is made. Remember that there -# may be multiple threads running, which might require up to -# buffer-limit Kbytes each, unless you have enabled -# "single-threaded" above. -# -buffer-limit 4096 -# -# 4.9. enable-proxy-authentication-forwarding -# ============================================ -# -# Specifies: -# -# Whether or not proxy authentication through Privoxy should -# work. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Proxy authentication headers are removed. -# -# Notes: -# -# Privoxy itself does not support proxy authentication, but can -# allow clients to authenticate against Privoxy's parent proxy. -# -# By default Privoxy (3.0.21 and later) don't do that and remove -# Proxy-Authorization headers in requests and Proxy-Authenticate -# headers in responses to make it harder for malicious sites to -# trick inexperienced users into providing login information. -# -# If this option is enabled the headers are forwarded. -# -# Enabling this option is not recommended if there is no parent -# proxy that requires authentication or if the local network -# between Privoxy and the parent proxy isn't trustworthy. If -# proxy authentication is only required for some requests, it is -# recommended to use a client header filter to remove the -# authentication headers for requests where they aren't needed. -# -enable-proxy-authentication-forwarding 0 -# -# 5. FORWARDING -# ============== -# -# This feature allows routing of HTTP requests through a chain of -# multiple proxies. -# -# Forwarding can be used to chain Privoxy with a caching proxy to -# speed up browsing. Using a parent proxy may also be necessary if -# the machine that Privoxy runs on has no direct Internet access. -# -# Note that parent proxies can severely decrease your privacy level. -# For example a parent proxy could add your IP address to the -# request headers and if it's a caching proxy it may add the "Etag" -# header to revalidation requests again, even though you configured -# Privoxy to remove it. It may also ignore Privoxy's header time -# randomization and use the original values which could be used by -# the server as cookie replacement to track your steps between -# visits. -# -# Also specified here are SOCKS proxies. Privoxy supports the SOCKS -# 4 and SOCKS 4A protocols. -# -# -# 5.1. forward -# ============= -# -# Specifies: -# -# To which parent HTTP proxy specific requests should be routed. -# -# Type of value: -# -# target_pattern http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent[:port] is the DNS name or IP -# address of the parent HTTP proxy through which the requests -# should be forwarded, optionally followed by its listening port -# (default: 8000). Use a single dot (.) to denote "no -# forwarding". -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use parent HTTP proxies. -# -# Notes: -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made directly to the web servers. -# -# http_parent can be a numerical IPv6 address (if RFC 3493 is -# implemented). To prevent clashes with the port delimiter, the -# whole IP address has to be put into brackets. On the other -# hand a target_pattern containing an IPv6 address has to be put -# into angle brackets (normal brackets are reserved for regular -# expressions already). -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# Examples: -# -# Everything goes to an example parent proxy, except SSL on port -# 443 (which it doesn't handle): -# -# forward / parent-proxy.example.org:8080 -# forward :443 . -# -# Everything goes to our example ISP's caching proxy, except for -# requests to that ISP's sites: -# -# forward / caching-proxy.isp.example.net:8000 -# forward .isp.example.net . -# -# Parent proxy specified by an IPv6 address: -# -# forward / [2001:DB8::1]:8000 -# -# Suppose your parent proxy doesn't support IPv6: -# -# forward / parent-proxy.example.org:8000 -# forward ipv6-server.example.org . -# forward <[2-3][0-9a-f][0-9a-f][0-9a-f]:*> . -forward / {{ local_service_ip }}:8080 -forward :443 . -# -# -# 5.2. forward-socks4, forward-socks4a, forward-socks5 and forward-socks5t -# ========================================================================= -# -# Specifies: -# -# Through which SOCKS proxy (and optionally to which parent HTTP -# proxy) specific requests should be routed. -# -# Type of value: -# -# target_pattern socks_proxy[:port] http_parent[:port] -# -# where target_pattern is a URL pattern that specifies to which -# requests (i.e. URLs) this forward rule shall apply. Use / to -# denote "all URLs". http_parent and socks_proxy are IP -# addresses in dotted decimal notation or valid DNS names ( -# http_parent may be "." to denote "no HTTP forwarding"), and -# the optional port parameters are TCP ports, i.e. integer -# values from 1 to 65535 -# -# Default value: -# -# Unset -# -# Effect if unset: -# -# Don't use SOCKS proxies. -# -# Notes: -# -# Multiple lines are OK, they are checked in sequence, and the -# last match wins. -# -# The difference between forward-socks4 and forward-socks4a is -# that in the SOCKS 4A protocol, the DNS resolution of the -# target hostname happens on the SOCKS server, while in SOCKS 4 -# it happens locally. -# -# With forward-socks5 the DNS resolution will happen on the -# remote server as well. -# -# forward-socks5t works like vanilla forward-socks5 but lets -# Privoxy additionally use Tor-specific SOCKS extensions. -# Currently the only supported SOCKS extension is optimistic -# data which can reduce the latency for the first request made -# on a newly created connection. -# -# socks_proxy and http_parent can be a numerical IPv6 address -# (if RFC 3493 is implemented). To prevent clashes with the port -# delimiter, the whole IP address has to be put into brackets. -# On the other hand a target_pattern containing an IPv6 address -# has to be put into angle brackets (normal brackets are -# reserved for regular expressions already). -# -# If http_parent is ".", then requests are not forwarded to -# another HTTP proxy but are made (HTTP-wise) directly to the -# web servers, albeit through a SOCKS proxy. -# -# Examples: -# -# From the company example.com, direct connections are made to -# all "internal" domains, but everything outbound goes through -# their ISP's proxy by way of example.com's corporate SOCKS 4A -# gateway to the Internet. -# -# forward-socks4a / socks-gw.example.com:1080 www-cache.isp.example.net:8080 -# forward .example.com . -# -# A rule that uses a SOCKS 4 gateway for all destinations but no -# HTTP parent looks like this: -# -# forward-socks4 / socks-gw.example.com:1080 . -# -# To chain Privoxy and Tor, both running on the same system, you -# would use something like: -# -# forward-socks5t / 127.0.0.1:9050 . -# -# Note that if you got Tor through one of the bundles, you may -# have to change the port from 9050 to 9150 (or even another -# one). For details, please check the documentation on the Tor -# website. -# -# The public Tor network can't be used to reach your local -# network, if you need to access local servers you therefore -# might want to make some exceptions: -# -# forward 192.168.*.*/ . -# forward 10.*.*.*/ . -# forward 127.*.*.*/ . -# -# Unencrypted connections to systems in these address ranges -# will be as (un)secure as the local network is, but the -# alternative is that you can't reach the local network through -# Privoxy at all. Of course this may actually be desired and -# there is no reason to make these exceptions if you aren't sure -# you need them. -# -# If you also want to be able to reach servers in your local -# network by using their names, you will need additional -# exceptions that look like this: -# -# forward localhost/ . -# -# -# 5.3. forwarded-connect-retries -# =============================== -# -# Specifies: -# -# How often Privoxy retries if a forwarded connection request -# fails. -# -# Type of value: -# -# Number of retries. -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Connections forwarded through other proxies are treated like -# direct connections and no retry attempts are made. -# -# Notes: -# -# forwarded-connect-retries is mainly interesting for socks4a -# connections, where Privoxy can't detect why the connections -# failed. The connection might have failed because of a DNS -# timeout in which case a retry makes sense, but it might also -# have failed because the server doesn't exist or isn't -# reachable. In this case the retry will just delay the -# appearance of Privoxy's error message. -# -# Note that in the context of this option, "forwarded -# connections" includes all connections that Privoxy forwards -# through other proxies. This option is not limited to the HTTP -# CONNECT method. -# -# Only use this option, if you are getting lots of -# forwarding-related error messages that go away when you try -# again manually. Start with a small value and check Privoxy's -# logfile from time to time, to see how many retries are usually -# needed. -# -# Examples: -# -# forwarded-connect-retries 1 -# -forwarded-connect-retries 0 -# -# 6. MISCELLANEOUS -# ================= -# -# 6.1. accept-intercepted-requests -# ================================= -# -# Specifies: -# -# Whether intercepted requests should be treated as valid. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Only proxy requests are accepted, intercepted requests are -# treated as invalid. -# -# Notes: -# -# If you don't trust your clients and want to force them to use -# Privoxy, enable this option and configure your packet filter -# to redirect outgoing HTTP connections into Privoxy. -# -# Note that intercepting encrypted connections (HTTPS) isn't -# supported. -# -# Make sure that Privoxy's own requests aren't redirected as -# well. Additionally take care that Privoxy can't intentionally -# connect to itself, otherwise you could run into redirection -# loops if Privoxy's listening port is reachable by the outside -# or an attacker has access to the pages you visit. -# -# Examples: -# -# accept-intercepted-requests 1 -# -accept-intercepted-requests 0 -# -# 6.2. allow-cgi-request-crunching -# ================================= -# -# Specifies: -# -# Whether requests to Privoxy's CGI pages can be blocked or -# redirected. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy ignores block and redirect actions for its CGI pages. -# -# Notes: -# -# By default Privoxy ignores block or redirect actions for its -# CGI pages. Intercepting these requests can be useful in -# multi-user setups to implement fine-grained access control, -# but it can also render the complete web interface useless and -# make debugging problems painful if done without care. -# -# Don't enable this option unless you're sure that you really -# need it. -# -# Examples: -# -# allow-cgi-request-crunching 1 -# -allow-cgi-request-crunching 0 -# -# 6.3. split-large-forms -# ======================= -# -# Specifies: -# -# Whether the CGI interface should stay compatible with broken -# HTTP clients. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# The CGI form generate long GET URLs. -# -# Notes: -# -# Privoxy's CGI forms can lead to rather long URLs. This isn't a -# problem as far as the HTTP standard is concerned, but it can -# confuse clients with arbitrary URL length limitations. -# -# Enabling split-large-forms causes Privoxy to divide big forms -# into smaller ones to keep the URL length down. It makes -# editing a lot less convenient and you can no longer submit all -# changes at once, but at least it works around this browser -# bug. -# -# If you don't notice any editing problems, there is no reason -# to enable this option, but if one of the submit buttons -# appears to be broken, you should give it a try. -# -# Examples: -# -# split-large-forms 1 -# -split-large-forms 0 -# -# 6.4. keep-alive-timeout -# ======================== -# -# Specifies: -# -# Number of seconds after which an open connection will no -# longer be reused. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not kept alive. -# -# Notes: -# -# This option allows clients to keep the connection to Privoxy -# alive. If the server supports it, Privoxy will keep the -# connection to the server alive as well. Under certain -# circumstances this may result in speed-ups. -# -# By default, Privoxy will close the connection to the server if -# the client connection gets closed, or if the specified timeout -# has been reached without a new request coming in. This -# behaviour can be changed with the connection-sharing option. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Note that a timeout of five seconds as used in the default -# configuration file significantly decreases the number of -# connections that will be reused. The value is used because -# some browsers limit the number of connections they open to a -# single host and apply the same limit to proxies. This can -# result in a single website "grabbing" all the connections the -# browser allows, which means connections to other websites -# can't be opened until the connections currently in use time -# out. -# -# Several users have reported this as a Privoxy bug, so the -# default value has been reduced. Consider increasing it to 300 -# seconds or even more if you think your browser can handle it. -# If your browser appears to be hanging, it probably can't. -# -# Examples: -# -# keep-alive-timeout 300 -# -keep-alive-timeout 5 -# -# 6.5. tolerate-pipelining -# ========================= -# -# Specifies: -# -# Whether or not pipelined requests should be served. -# -# Type of value: -# -# 0 or 1. -# -# Default value: -# -# None -# -# Effect if unset: -# -# If Privoxy receives more than one request at once, it -# terminates the client connection after serving the first one. -# -# Notes: -# -# Privoxy currently doesn't pipeline outgoing requests, thus -# allowing pipelining on the client connection is not guaranteed -# to improve the performance. -# -# By default Privoxy tries to discourage clients from pipelining -# by discarding aggressively pipelined requests, which forces -# the client to resend them through a new connection. -# -# This option lets Privoxy tolerate pipelining. Whether or not -# that improves performance mainly depends on the client -# configuration. -# -# If you are seeing problems with pages not properly loading, -# disabling this option could work around the problem. -# -# Examples: -# -# tolerate-pipelining 1 -# -tolerate-pipelining 1 -# -# 6.6. default-server-timeout -# ============================ -# -# Specifies: -# -# Assumed server-side keep-alive timeout if not specified by the -# server. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections for which the server didn't specify the keep-alive -# timeout are not reused. -# -# Notes: -# -# Enabling this option significantly increases the number of -# connections that are reused, provided the keep-alive-timeout -# option is also enabled. -# -# While it also increases the number of connections problems -# when Privoxy tries to reuse a connection that already has been -# closed on the server side, or is closed while Privoxy is -# trying to reuse it, this should only be a problem if it -# happens for the first request sent by the client. If it -# happens for requests on reused client connections, Privoxy -# will simply close the connection and the client is supposed to -# retry the request without bothering the user. -# -# Enabling this option is therefore only recommended if the -# connection-sharing option is disabled. -# -# It is an error to specify a value larger than the -# keep-alive-timeout value. -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support. -# -# Examples: -# -# default-server-timeout 60 -# -#default-server-timeout 60 -# -# 6.7. connection-sharing -# ======================== -# -# Specifies: -# -# Whether or not outgoing connections that have been kept alive -# should be shared between different incoming connections. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# None -# -# Effect if unset: -# -# Connections are not shared. -# -# Notes: -# -# This option has no effect if Privoxy has been compiled without -# keep-alive support, or if it's disabled. -# -# Notes: -# -# Note that reusing connections doesn't necessary cause -# speedups. There are also a few privacy implications you should -# be aware of. -# -# If this option is effective, outgoing connections are shared -# between clients (if there are more than one) and closing the -# browser that initiated the outgoing connection does no longer -# affect the connection between Privoxy and the server unless -# the client's request hasn't been completed yet. -# -# If the outgoing connection is idle, it will not be closed -# until either Privoxy's or the server's timeout is reached. -# While it's open, the server knows that the system running -# Privoxy is still there. -# -# If there are more than one client (maybe even belonging to -# multiple users), they will be able to reuse each others -# connections. This is potentially dangerous in case of -# authentication schemes like NTLM where only the connection is -# authenticated, instead of requiring authentication for each -# request. -# -# If there is only a single client, and if said client can keep -# connections alive on its own, enabling this option has next to -# no effect. If the client doesn't support connection -# keep-alive, enabling this option may make sense as it allows -# Privoxy to keep outgoing connections alive even if the client -# itself doesn't support it. -# -# You should also be aware that enabling this option increases -# the likelihood of getting the "No server or forwarder data" -# error message, especially if you are using a slow connection -# to the Internet. -# -# This option should only be used by experienced users who -# understand the risks and can weight them against the benefits. -# -# Examples: -# -# connection-sharing 1 -# -#connection-sharing 1 -# -# 6.8. socket-timeout -# ==================== -# -# Specifies: -# -# Number of seconds after which a socket times out if no data is -# received. -# -# Type of value: -# -# Time in seconds. -# -# Default value: -# -# None -# -# Effect if unset: -# -# A default value of 300 seconds is used. -# -# Notes: -# -# The default is quite high and you probably want to reduce it. -# If you aren't using an occasionally slow proxy like Tor, -# reducing it to a few seconds should be fine. -# -# Examples: -# -# socket-timeout 300 -# -socket-timeout 300 -# -# 6.9. max-client-connections -# ============================ -# -# Specifies: -# -# Maximum number of client connections that will be served. -# -# Type of value: -# -# Positive number. -# -# Default value: -# -# 128 -# -# Effect if unset: -# -# Connections are served until a resource limit is reached. -# -# Notes: -# -# Privoxy creates one thread (or process) for every incoming -# client connection that isn't rejected based on the access -# control settings. -# -# If the system is powerful enough, Privoxy can theoretically -# deal with several hundred (or thousand) connections at the -# same time, but some operating systems enforce resource limits -# by shutting down offending processes and their default limits -# may be below the ones Privoxy would require under heavy load. -# -# Configuring Privoxy to enforce a connection limit below the -# thread or process limit used by the operating system makes -# sure this doesn't happen. Simply increasing the operating -# system's limit would work too, but if Privoxy isn't the only -# application running on the system, you may actually want to -# limit the resources used by Privoxy. -# -# If Privoxy is only used by a single trusted user, limiting the -# number of client connections is probably unnecessary. If there -# are multiple possibly untrusted users you probably still want -# to additionally use a packet filter to limit the maximal -# number of incoming connections per client. Otherwise a -# malicious user could intentionally create a high number of -# connections to prevent other users from using Privoxy. -# -# Obviously using this option only makes sense if you choose a -# limit below the one enforced by the operating system. -# -# One most POSIX-compliant systems Privoxy can't properly deal -# with more than FD_SETSIZE file descriptors at the same time -# and has to reject connections if the limit is reached. This -# will likely change in a future version, but currently this -# limit can't be increased without recompiling Privoxy with a -# different FD_SETSIZE limit. -# -# Examples: -# -# max-client-connections 256 -# -#max-client-connections 256 -# -# 6.10. handle-as-empty-doc-returns-ok -# ===================================== -# -# Specifies: -# -# The status code Privoxy returns for pages blocked with -# +handle-as-empty-document. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy returns a status 403(forbidden) for all blocked pages. -# -# Effect if set: -# -# Privoxy returns a status 200(OK) for pages blocked with -# +handle-as-empty-document and a status 403(Forbidden) for all -# other blocked pages. -# -# Notes: -# -# This directive was added as a work-around for Firefox bug -# 492459: "Websites are no longer rendered if SSL requests for -# JavaScripts are blocked by a proxy." -# (https://bugzilla.mozilla.org/show_bug.cgi?id=492459), the bug -# has been fixed for quite some time, but this directive is also -# useful to make it harder for websites to detect whether or not -# resources are being blocked. -# -#handle-as-empty-doc-returns-ok 1 -# -# 6.11. enable-compression -# ========================= -# -# Specifies: -# -# Whether or not buffered content is compressed before delivery. -# -# Type of value: -# -# 0 or 1 -# -# Default value: -# -# 0 -# -# Effect if unset: -# -# Privoxy does not compress buffered content. -# -# Effect if set: -# -# Privoxy compresses buffered content before delivering it to -# the client, provided the client supports it. -# -# Notes: -# -# This directive is only supported if Privoxy has been compiled -# with FEATURE_COMPRESSION, which should not to be confused with -# FEATURE_ZLIB. -# -# Compressing buffered content is mainly useful if Privoxy and -# the client are running on different systems. If they are -# running on the same system, enabling compression is likely to -# slow things down. If you didn't measure otherwise, you should -# assume that it does and keep this option disabled. -# -# Privoxy will not compress buffered content below a certain -# length. -# -#enable-compression 1 -# -# 6.12. compression-level -# ======================== -# -# Specifies: -# -# The compression level that is passed to the zlib library when -# compressing buffered content. -# -# Type of value: -# -# Positive number ranging from 0 to 9. -# -# Default value: -# -# 1 -# -# Notes: -# -# Compressing the data more takes usually longer than -# compressing it less or not compressing it at all. Which level -# is best depends on the connection between Privoxy and the -# client. If you can't be bothered to benchmark it for yourself, -# you should stick with the default and keep compression -# disabled. -# -# If compression is disabled, the compression level is -# irrelevant. -# -# Examples: -# -# # Best speed (compared to the other levels) -# compression-level 1 -# -# # Best compression -# compression-level 9 -# -# # No compression. Only useful for testing as the added header -# # slightly increases the amount of data that has to be sent. -# # If your benchmark shows that using this compression level -# # is superior to using no compression at all, the benchmark -# # is likely to be flawed. -# compression-level 0 -# -# -#compression-level 1 -# -# 6.13. client-header-order -# ========================== -# -# Specifies: -# -# The order in which client headers are sorted before forwarding -# them. -# -# Type of value: -# -# Client header names delimited by spaces or tabs -# -# Default value: -# -# None -# -# Notes: -# -# By default Privoxy leaves the client headers in the order they -# were sent by the client. Headers are modified in-place, new -# headers are added at the end of the already existing headers. -# -# The header order can be used to fingerprint client requests -# independently of other headers like the User-Agent. -# -# This directive allows to sort the headers differently to -# better mimic a different User-Agent. Client headers will be -# emitted in the order given, headers whose name isn't -# explicitly specified are added at the end. -# -# Note that sorting headers in an uncommon way will make -# fingerprinting actually easier. Encrypted headers are not -# affected by this directive. -# -#client-header-order Host \ -# Accept \ -# Accept-Language \ -# Accept-Encoding \ -# Proxy-Connection \ -# Referer \ -# Cookie \ -# DNT \ -# If-Modified-Since \ -# Cache-Control \ -# Content-Length \ -# Content-Type -# -# -# 7. WINDOWS GUI OPTIONS -# ======================= -# -# Privoxy has a number of options specific to the Windows GUI -# interface: -# -# -# -# If "activity-animation" is set to 1, the Privoxy icon will animate -# when "Privoxy" is active. To turn off, set to 0. -# -#activity-animation 1 -# -# -# -# If "log-messages" is set to 1, Privoxy copies log messages to the -# console window. The log detail depends on the debug directive. -# -#log-messages 1 -# -# -# -# If "log-buffer-size" is set to 1, the size of the log buffer, i.e. -# the amount of memory used for the log messages displayed in the -# console window, will be limited to "log-max-lines" (see below). -# -# Warning: Setting this to 0 will result in the buffer to grow -# infinitely and eat up all your memory! -# -#log-buffer-size 1 -# -# -# -# log-max-lines is the maximum number of lines held in the log -# buffer. See above. -# -#log-max-lines 200 -# -# -# -# If "log-highlight-messages" is set to 1, Privoxy will highlight -# portions of the log messages with a bold-faced font: -# -#log-highlight-messages 1 -# -# -# -# The font used in the console window: -# -#log-font-name Comic Sans MS -# -# -# -# Font size used in the console window: -# -#log-font-size 8 -# -# -# -# "show-on-task-bar" controls whether or not Privoxy will appear as -# a button on the Task bar when minimized: -# -#show-on-task-bar 0 -# -# -# -# If "close-button-minimizes" is set to 1, the Windows close button -# will minimize Privoxy instead of closing the program (close with -# the exit option on the File menu). -# -#close-button-minimizes 1 -# -# -# -# The "hide-console" option is specific to the MS-Win console -# version of Privoxy. If this option is used, Privoxy will -# disconnect from and hide the command console. -# -#hide-console -# -# -# diff --git a/roles/proxy/templates/usr.sbin.privoxy.j2 b/roles/proxy/templates/usr.sbin.privoxy.j2 deleted file mode 100644 index 5f8d9ddf..00000000 --- a/roles/proxy/templates/usr.sbin.privoxy.j2 +++ /dev/null @@ -1,15 +0,0 @@ -#include - -/usr/sbin/privoxy { - #include - #include - - capability setgid, - capability setuid, - - /etc/privoxy/* r, - /etc/privoxy/templates/* r, - /run/privoxy.pid w, - /var/log/privoxy/logfile w, - -} diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index aed75763..3ccef364 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,3 +1,5 @@ +--- + - name: Install tools apt: name="{{ item }}" state=latest with_items: diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 578fb793..5e1f2739 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -1,8 +1,5 @@ --- -- set_fact: - IP_subject_alt_name: "{{ IP_subject_alt_name }}" - - name: Ensure that the sshd_config file has desired options blockinfile: dest: /etc/ssh/sshd_config diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 006479d7..87856355 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,25 +1,4 @@ -- name: Gather Facts - setup: - -- name: Enable IPv6 - set_fact: - ipv6_support: true - when: ansible_default_ipv6.gateway is defined - -- name: Generate password for the CA key - shell: > - openssl rand -hex 16 - register: CA_password - -- set_fact: - easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" - easyrsa_CA_password: "{{ CA_password.stdout }}" - IP_subject_alt_name: "{{ IP_subject_alt_name }}" - -- name: Change the algorithm to RSA - set_fact: - algo_params: "rsa:2048" - when: Win10_Enabled is defined and Win10_Enabled == "Y" +--- - name: Ensure that the strongswan group exist group: name=strongswan state=present diff --git a/users.yml b/users.yml index b4cdf742..d6e9399a 100644 --- a/users.yml +++ b/users.yml @@ -40,10 +40,6 @@ - name: Common pre-tasks include: playbooks/common.yml - - set_fact: - IP_subject_alt_name: "{{ IP_subject }}" - easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" - roles: - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } From 0e4aace6b6e5490626c3738affab4b464fdcf49b Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 20 Apr 2017 18:00:55 -0400 Subject: [PATCH 473/769] Update deploy-to-ubuntu.md --- docs/deploy-to-ubuntu.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index d25aaca8..0d166523 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,6 +1,8 @@ # Local deployment -It is possible to download the Algo scripts to your own Ubuntu server and run the scripts locally. You need to install Ansible to run Algo on Ubuntu. Installing ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. Therefore, to use apt you must use the ansible PPA, and using a PPA requires installing `software-properties-common`. +It is possible to download the Algo scripts to your own Ubuntu 16.04 server and run the scripts locally. + +In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. tl;dr: @@ -11,4 +13,4 @@ git clone https://github.com/trailofbits/algo cd algo && ./algo ``` -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. \ No newline at end of file +**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. From 76cdc695482ac2ace7e0d733e4ee89a1c837993c Mon Sep 17 00:00:00 2001 From: Andy Boutte Date: Thu, 20 Apr 2017 15:04:57 -0700 Subject: [PATCH 474/769] CF tested and working for EC2 deployment (#431) * AWS CloudFormation #132 * IPv6 EC2 draft * CF tested and working for EC2 deployment * IPv6 Implementation, EC2, Cloudformation * Fixed ipv6 networking * adding ip6tables rule for DHCP on AWS --- roles/cloud-ec2/tasks/cloudformation.yml | 18 +++ roles/cloud-ec2/tasks/encrypt_image.yml | 8 +- roles/cloud-ec2/tasks/main.yml | 96 +----------- roles/cloud-ec2/templates/stack.yml.j2 | 192 +++++++++++++++++++++++ roles/vpn/templates/rules.v6.j2 | 2 + 5 files changed, 221 insertions(+), 95 deletions(-) create mode 100644 roles/cloud-ec2/tasks/cloudformation.yml create mode 100644 roles/cloud-ec2/templates/stack.yml.j2 diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml new file mode 100644 index 00000000..1f24b007 --- /dev/null +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -0,0 +1,18 @@ +--- + +- name: Make a cloudformation template + template: + src: stack.yml.j2 + dest: "configs/{{ aws_server_name }}.yml" + +- name: Deploy the template + cloudformation: + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" + stack_name: "{{ stack_name }}" + state: "present" + region: "{{ region }}" + template: "configs/{{ aws_server_name }}.yml" + tags: + Environment: Algo + register: stack \ No newline at end of file diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml index da46534a..11779ea4 100644 --- a/roles/cloud-ec2/tasks/encrypt_image.yml +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -1,7 +1,7 @@ - name: Check if the encrypted image already exist ec2_ami_find: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" owner: self sort: creationDate sort_order: descending @@ -18,8 +18,8 @@ - name: Copy to an encrypted image ec2_ami_copy: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'))}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'))}}" + aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" + aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" encrypted: yes name: algo kms_key_id: "{{ kms_key_id | default(omit) }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index dfb3b1f9..46d71bb8 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,6 +1,7 @@ - set_fact: access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + stack_name: "{{ aws_server_name | replace('.', '-') }}" - name: Locate official Ubuntu 16.04 AMI for region ec2_ami_find: @@ -20,107 +21,20 @@ - include: encrypt_image.yml tags: [encrypted] -- name: Add ssh public key - ec2_key: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - name: VPNKEY - region: "{{ region }}" - key_material: "{{ item }}" - with_file: "{{ SSH_keys.public }}" - register: keypair - -- name: Configure EC2 virtual private clouds - ec2_vpc: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - state: present - resource_tags: { "Environment":"Algo" } - region: "{{ region }}" - cidr_block: "{{ ec2_vpc_nets.cidr_block }}" - internet_gateway: yes - subnets: - - cidr: "{{ ec2_vpc_nets.subnet_cidr }}" - resource_tags: { "Environment":"Algo" } - register: vpc - -- name: Set up Public Subnets Route Table - ec2_vpc_route_table: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - vpc_id: "{{ vpc.vpc_id }}" - region: "{{ region }}" - state: present - tags: - Environment: Algo - subnets: - - "{{ ec2_vpc_nets.subnet_cidr }}" - routes: - - dest: 0.0.0.0/0 - gateway_id: "{{ vpc.igw_id }}" - register: public_rt - -- name: Configure EC2 security group - ec2_group: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - name: vpn-secgroup - description: Security group for VPN servers - region: "{{ region }}" - vpc_id: "{{ vpc.vpc_id }}" - rules: - - proto: udp - from_port: 4500 - to_port: 4500 - cidr_ip: 0.0.0.0/0 - - proto: udp - from_port: 500 - to_port: 500 - cidr_ip: 0.0.0.0/0 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 0.0.0.0/0 - rules_egress: - - proto: all - from_port: 0-65535 - to_port: 0-65535 - cidr_ip: 0.0.0.0/0 - -- name: Launch instance - ec2: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - keypair: "VPNKEY" - vpc_subnet_id: "{{ vpc.subnets[0].id }}" - group: vpn-secgroup - instance_type: "{{ cloud_providers.ec2.size }}" - image: "{{ ami_image }}" - wait: true - region: "{{ region }}" - instance_tags: - Name: "{{ aws_server_name }}" - Environment: Algo - exact_count: 1 - count_tag: - Name: "{{ aws_server_name }}" - assign_public_ip: yes - instance_initiated_shutdown_behavior: terminate - register: ec2 +- include: cloudformation.yml - name: Add new instance to host group add_host: - hostname: "{{ item.public_ip }}" + hostname: "{{ stack.stack_outputs.PublicIP }}" groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: ec2 - ipv6_support: no - with_items: "{{ ec2.tagged_instances }}" + ipv6_support: yes - set_fact: - cloud_instance_ip: "{{ ec2.tagged_instances[0].public_ip }}" + cloud_instance_ip: "{{ stack.stack_outputs.PublicIP }}" - name: Get EC2 instances ec2_remote_facts: diff --git a/roles/cloud-ec2/templates/stack.yml.j2 b/roles/cloud-ec2/templates/stack.yml.j2 new file mode 100644 index 00000000..1678413b --- /dev/null +++ b/roles/cloud-ec2/templates/stack.yml.j2 @@ -0,0 +1,192 @@ +--- + +AWSTemplateFormatVersion: '2010-09-09' +Description: 'Algo VPN stack' +Resources: + + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: {{ ec2_vpc_nets.cidr_block }} + EnableDnsSupport: true + EnableDnsHostnames: true + InstanceTenancy: default + Tags: + - Key: Name + Value: Algo + - Key: Environment + Value: Algo + + VPCIPv6: + Type: AWS::EC2::VPCCidrBlock + Properties: + AmazonProvidedIpv6CidrBlock: true + VpcId: !Ref VPC + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Environment + Value: Algo + - Key: Name + Value: Algo + + Subnet: + Type: AWS::EC2::Subnet + Properties: + CidrBlock: {{ ec2_vpc_nets.subnet_cidr }} + MapPublicIpOnLaunch: true + Tags: + - Key: Environment + Value: Algo + - Key: Name + Value: Algo + VpcId: !Ref VPC + + VPCGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + RouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Environment + Value: Algo + - Key: Name + Value: Algo + + Route: + Type: AWS::EC2::Route + DependsOn: + - InternetGateway + - RouteTable + - VPCGatewayAttachment + Properties: + RouteTableId: !Ref RouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + RouteIPv6: + Type: AWS::EC2::Route + DependsOn: + - InternetGateway + - RouteTable + - VPCGatewayAttachment + Properties: + RouteTableId: !Ref RouteTable + DestinationIpv6CidrBlock: "::/0" + GatewayId: !Ref InternetGateway + + SubnetIPv6: + Type: AWS::EC2::SubnetCidrBlock + DependsOn: + - RouteIPv6 + - VPC + - VPCIPv6 + Properties: + Ipv6CidrBlock: + "Fn::Join": + - "" + - - !Select [0, !Split [ "::", !Select [0, !GetAtt VPC.Ipv6CidrBlocks] ]] + - "::dead:beef/64" + SubnetId: !Ref Subnet + + RouteSubnet: + Type: "AWS::EC2::SubnetRouteTableAssociation" + DependsOn: + - RouteTable + - Subnet + - Route + Properties: + RouteTableId: !Ref RouteTable + SubnetId: !Ref Subnet + + InstanceSecurityGroup: + Type: AWS::EC2::SecurityGroup + DependsOn: + - Subnet + Properties: + VpcId: !Ref VPC + GroupDescription: Enable SSH and IPsec + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: '22' + ToPort: '22' + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: '500' + ToPort: '500' + CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: '4500' + ToPort: '4500' + CidrIp: 0.0.0.0/0 + Tags: + - Key: Name + Value: Algo + - Key: Environment + Value: Algo + + EC2Instance: + Type: AWS::EC2::Instance + DependsOn: + - SubnetIPv6 + - Subnet + - InstanceSecurityGroup + Metadata: + AWS::CloudFormation::Init: + config: + users: + ubuntu: + groups: + - "sudo" + homeDir: "/home/ubuntu/" + files: + /home/ubuntu/.ssh/authorized_keys: + content: {{ lookup('file', SSH_keys.public) }} + mode: "000644" + owner: "ubuntu" + group: "ubuntu" + Properties: + InstanceType: {{ cloud_providers.ec2.size }} + InstanceInitiatedShutdownBehavior: terminate + SecurityGroupIds: + - Ref: InstanceSecurityGroup + ImageId: {{ ami_image }} + SubnetId: !Ref Subnet + Ipv6AddressCount: 1 + UserData: + "Fn::Base64": + !Sub | + #!/bin/bash -xe + # http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-migrate-ipv6.html + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1013597 + cat < /etc/network/interfaces.d/60-default-with-ipv6.cfg + iface eth0 inet6 dhcp + up sysctl net.ipv6.conf.\$IFACE.accept_ra=2 + pre-down ip link set dev \$IFACE up + EOF + ifdown eth0; ifup eth0 + dhclient -6 + apt-get update + apt-get -y install python-setuptools + easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz + cfn-init -v --stack {{ stack_name }} --resource EC2Instance --region {{ region }} + cfn-signal -e $? --stack {{ stack_name }} --resource EC2Instance --region {{ region }} + Tags: + - Key: Name + Value: Algo + - Key: Environment + Value: Algo + +Outputs: + PublicIP: + Value: + Fn::GetAtt: + - EC2Instance + - PublicIp diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 0eda48fc..f8d9593f 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -31,6 +31,8 @@ COMMIT -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT +# DHCP in AWS +-A INPUT -m state --state NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any From 22e145c2411e29bb00ab06145d73c5e3184148cb Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 20 Apr 2017 18:15:31 -0400 Subject: [PATCH 475/769] Update documentation to include minimum required IAM policy (#461) * Updating documentation to include minimum required IAM polcy. Closes * Slightly more concise --- algo | 3 +- docs/deploy-with-ansible.md | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/algo b/algo index a89c704b..0ca82bd7 100755 --- a/algo +++ b/algo @@ -232,14 +232,13 @@ EXTRA_VARS="do_access_token=$do_access_token do_server_name=$do_server_name do_r ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached +Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-with-ansible.md). $ADDITIONAL_PROMPT [AKIA...]: " -rs aws_access_key read -p " Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use either your root key (recommended) or an IAM user with an acceptable policy attached $ADDITIONAL_PROMPT [ABCD...]: " -rs aws_secret_key diff --git a/docs/deploy-with-ansible.md b/docs/deploy-with-ansible.md index e1a86ff2..54154bbb 100644 --- a/docs/deploy-with-ansible.md +++ b/docs/deploy-with-ansible.md @@ -103,6 +103,70 @@ Additional tags: - [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) (enabled by default) +#### Minimum required IAM permissions for deployment: + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PreDeployment", + "Effect": "Allow", + "Action": [ + "ec2:DescribeImages", + "ec2:DescribeKeyPairs", + "ec2:ImportKeyPair" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "DeployCloudFormationStack", + "Effect": "Allow", + "Action": [ + "cloudformation:CreateStack", + "cloudformation:DescribeStacks", + "cloudformation:CreateStacks", + "cloudformation:DescribeStackEvents", + "cloudformation:ListStackResources" + ], + "Resource": [ + "*" + ] + }, + { + "Sid": "CloudFormationEC2Access", + "Effect": "Allow", + "Action": [ + "ec2:CreateInternetGateway", + "ec2:DescribeVpcs", + "ec2:CreateVpc", + "ec2:DescribeInternetGateways", + "ec2:ModifyVpcAttribute", + "ec2:createTags", + "ec2:CreateSubnet", + "ec2:Associate*", + "ec2:CreateRouteTable", + "ec2:AttachInternetGateway", + "ec2:DescribeRouteTables", + "ec2:DescribeSubnets", + "ec2:ModifySubnetAttribute", + "ec2:CreateRoute", + "ec2:CreateSecurityGroup", + "ec2:DescribeSecurityGroups", + "ec2:AuthorizeSecurityGroupIngress", + "ec2:RunInstances", + "ec2:DescribeInstances" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + ### Google Compute Engine Required variables: From b94b455aba081e3fc4d35378253e61b9c01c3243 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Thu, 20 Apr 2017 18:28:16 -0400 Subject: [PATCH 476/769] typo --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 635ae52a..719fc03a 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. * Client setup - Setup [Windows](client-windows.md) clients - Setup [Android](client-android.md) clients - - Setup [Generic/Linux](client-generic.md) clients with Ansible + - Setup [Generic/Linux](client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](cloud-azure.md) - Deploy to an [unsupported cloud provider](cloud-unsupported.md) diff --git a/docs/index.md b/docs/index.md index 37d4f73f..d56e81b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ * Client setup - Setup [Windows](client-windows.md) clients - Setup [Android](client-android.md) clients - - Setup [Generic/Linux](client-generic.md) clients with Ansible + - Setup [Generic/Linux](client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](cloud-azure.md) - Deploy to an [unsupported cloud provider](cloud-unsupported.md) From 39822a1b4e44cb3d88270fec836bad5158ada742 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 21 Apr 2017 12:20:33 -0400 Subject: [PATCH 477/769] Add back table of contents (#463) * toc * shift left * derp --- docs/troubleshooting.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a72c500a..7753b98a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,24 @@ # Troubleshooting + * [Installation Problems](#installation-problems) + * [Error: "You have not agreed to the Xcode license agreements"](#error-you-have-not-agreed-to-the-xcode-license-agreements) + * [Error: checking whether the C compiler works... no](#error-checking-whether-the-c-compiler-works-no) + * [Error: "fatal error: 'openssl/opensslv.h' file not found"](#error-fatal-error-opensslopensslvh-file-not-found) + * [Error: "TypeError: must be str, not bytes"](#error-typeerror-must-be-str-not-bytes) + * [Error: "ansible-playbook: command not found"](#error-ansible-playbook-command-not-found) + * [Bad owner or permissions on .ssh](#bad-owner-or-permissions-on-ssh) + * [The region you want is not available](#the-region-you-want-is-not-available) + * [Connection Problems](#connection-problems) + * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) + * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) + * [Error: "The VPN Service payload could not be installed."](#error-the-vpn-service-payload-could-not-be-installed) + * [Little Snitch is broken when connected to the VPN](#little-snitch-is-broken-when-connected-to-the-vpn) + * [I can't get my router to connect to the Algo server](#i-cant-get-my-router-to-connect-to-the-algo-server) + * [I can't get Network Manager to connect to the Algo server](#i-cant-get-network-manager-to-connect-to-the-algo-server) + * [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) + * ["Error 809" or IKE_AUTH requests that never make it to the server](#error-809-or-ike_auth-requests-that-never-make-it-to-the-server) + * [I have a problem not covered here](#i-have-a-problem-not-covered-here) + ## Installation Problems Look here if you have a problem running the installer to set up a new Algo server. From f75c857656fba1f2538160c6908f7c3d1caa8ea4 Mon Sep 17 00:00:00 2001 From: Jay Little Date: Sat, 22 Apr 2017 14:00:16 -0400 Subject: [PATCH 478/769] Fix broken links. (#469) --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 719fc03a..985b8c7c 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [ansible-roles.md](docs/ansible-roles.md). +6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-with-ansible.md](docs/deploy-with-ansible.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. -You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](https://github.com/trailofbits/algo#configure-the-vpn-clients) below. +You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](#configure-the-vpn-clients) below. ``` "\"#----------------------------------------------------------------------#\"", @@ -84,7 +84,7 @@ You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to "\"#----------------------------------------------------------------------#\"", ``` -Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Advanced Usage](/docs/advanced-usage.md) documentation. +Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Deploy to Ubuntu](/docs/deploy-to-ubuntu.md) documentation. ## Configure the VPN Clients @@ -199,21 +199,21 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation * Setup instructions - - Documentation for avaialble [Ansible roles](setup-roles.md) - - Deploy from [RedHat/CentOS 6.x](setup-redhat-centos6.md) + - Documentation for avaialble [Ansible roles](docs/deploy-with-ansible.md) + - Deploy from [RedHat/CentOS 6.x](docs/setup-redhat-centos6.md) * Client setup - - Setup [Windows](client-windows.md) clients - - Setup [Android](client-android.md) clients - - Setup [Generic/Linux](client-linux.md) clients with Ansible + - Setup [Windows](docs/client-windows.md) clients + - Setup [Android](docs/client-android.md) clients + - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible * Cloud setup - - Configure [Azure](cloud-azure.md) - - Deploy to an [unsupported cloud provider](cloud-unsupported.md) + - Configure [Azure](docs/cloud-azure.md) + - Deploy to an [unsupported cloud provider](docs/cloud-unsupported.md) * Advanced Deployment - - Deploy to local [FreeBSD](deploy-to-freebsd.md) servers - - Deploy to local [Ubuntu 16.04](deploy-to-ubuntu.md) servers - - Deploy with [Ansible](deploy-with-ansible.md) -* [FAQ](faq.md) -* [Troubleshooting](troubleshooting.md) + - Deploy to local [FreeBSD](docs/deploy-to-freebsd.md) servers + - Deploy to local [Ubuntu 16.04](docs/deploy-to-ubuntu.md) servers + - Deploy with [Ansible](docs/deploy-with-ansible.md) +* [FAQ](docs/faq.md) +* [Troubleshooting](docs/troubleshooting.md) ## Endorsements From 3aa4b6e8df205bb47a9de32afae5358110eab840 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 22 Apr 2017 14:57:39 -0400 Subject: [PATCH 479/769] Add linters to our CI (#471) --- .travis.yml | 11 +++++++++++ README.md | 2 +- docs/faq.md | 4 ++-- docs/troubleshooting.md | 2 -- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b2a394e..ab4f62c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ addons: - lxc-templates - expect-dev - debootstrap + - shellcheck cache: directories: @@ -42,10 +43,20 @@ install: - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt + - pip install ansible-lint + +before_script: + - gem install awesome_bot script: + - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new +# - shellcheck algo +# - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" after_script: - ./tests/update-users.sh + +notifications: + email: false diff --git a/README.md b/README.md index 985b8c7c..b7572a76 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Algo VPN -[![TravisCI Status](https://travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) +[![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) [![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) diff --git a/docs/faq.md b/docs/faq.md index 8ff4a3be..dc49f106 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,7 +10,7 @@ ## Has Algo been audited? -No. This project is under [active development](https://github.com/trailofbits/algo/projects/1). We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). +No. This project is under active development. We're happy to [accept and fix issues](https://github.com/trailofbits/algo/issues) as they are identified. Use Algo at your own risk. If you find a security issue of any severity, please [contact us on Slack](https://empireslacking.herokuapp.com). ## Why aren't you using Tor? @@ -26,7 +26,7 @@ I would, but I don't know of any [suitable ones](https://github.com/trailofbits/ ## Why aren't you using OpenVPN? -OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](http://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](http://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). +OpenVPN does not have out-of-the-box client support on any major desktop or mobile operating system. This introduces user experience issues and requires the user to [update](https://www.exploit-db.com/exploits/34037/) and [maintain](https://www.exploit-db.com/exploits/20485/) the software themselves. OpenVPN depends on the security of [TLS](https://tools.ietf.org/html/rfc7457), both the [protocol](https://arstechnica.com/security/2016/08/new-attack-can-pluck-secrets-from-1-of-https-traffic-affects-top-sites/) and its [implementations](https://arstechnica.com/security/2014/04/confirmed-nasty-heartbleed-bug-exposes-openvpn-private-keys-too/), and we simply trust the server less due to [past](https://sweet32.info/) [security](https://github.com/ValdikSS/openvpn-fix-dns-leak-plugin/blob/master/README.md) [incidents](https://www.exploit-db.com/exploits/34879/). ## Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD? diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7753b98a..3ce2d768 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -36,8 +36,6 @@ You have not agreed to the Xcode license agreements, please run 'xcodebuild -lic No working compiler found, or bogus compiler options passed to the compiler from Python's distutils module. See the error messages above. - (If they are about -mno-fused-madd and you are on OS/X 10.8, - see http://stackoverflow.com/questions/22313407/ .) ---------------------------------------- Cleaning up... From cbb8237a4c920af0edfeddd294d3ac3266a48efa Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 22 Apr 2017 16:52:02 -0400 Subject: [PATCH 480/769] fix link (#472) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7572a76..86a74bea 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua python-setuptools \ python-virtualenv -y ``` - - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/server-redhat-centos6.md) + - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/setup-redhat-centos6.md) - Windows: See the [Windows documentation](docs/client-windows.md) 4. Install Algo's remaining dependencies for your operating system. Use the same terminal window as the previous step and run: From c3fcfe5d0d22ce9027f8743aae870dcabfaab98b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 22 Apr 2017 23:06:10 +0200 Subject: [PATCH 481/769] Let users choose the distro version #449 (#466) Make dpdaction great again add 1704 to travis Make EC2 image name more convenient modify apparmor profile --- .travis.yml | 1 + config.cfg | 10 ++++++++++ roles/cloud-azure/tasks/main.yml | 6 +----- roles/cloud-digitalocean/tasks/main.yml | 2 +- roles/cloud-ec2/tasks/main.yml | 6 +++--- roles/cloud-gce/tasks/main.yml | 2 +- roles/dns_adblocking/tasks/main.yml | 4 ++-- roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 | 2 ++ roles/vpn/tasks/main.yml | 5 +++++ roles/vpn/templates/ipsec.conf.j2 | 4 ++++ 10 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab4f62c2..8b304f7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ before_cache: env: - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial + - LXC_NAME=ubuntu1704 LXC_DISTRO=ubuntu LXC_RELEASE=zesty install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." diff --git a/config.cfg b/config.cfg index b0fdefe0..c63a46c8 100644 --- a/config.cfg +++ b/config.cfg @@ -61,10 +61,20 @@ SSH_keys: cloud_providers: azure: size: Basic_A0 + image: + offer: UbuntuServer + publisher: Canonical + sku: '16.04-LTS' # 16.04-LTS + version: latest digitalocean: size: 512mb + image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-04-x64 ec2: size: t2.micro + image: + name: "ubuntu-zesty-17.04" # ubuntu-xenial-16.04 / ubuntu-zesty-17.04 + owner: "099720109477" gce: size: f1-micro + image: ubuntu-1604 # ubuntu-1604 / ubuntu-1704 local: diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 252894b6..9048615f 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -89,11 +89,7 @@ Environment: Algo ssh_public_keys: - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } - image: - offer: UbuntuServer - publisher: Canonical - sku: '16.04-LTS' - version: latest + image: "{{ cloud_providers.azure.image }}" register: azure_rm_virtualmachine # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 15fbbd9d..897f52af 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -46,7 +46,7 @@ name: "{{ do_server_name }}" region_id: "{{ do_region }}" size_id: "{{ cloud_providers.digitalocean.size }}" - image_id: "ubuntu-16-04-x64" + image_id: "{{ cloud_providers.digitalocean.image }}" ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" unique_name: yes api_token: "{{ do_token }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 46d71bb8..b99255c9 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -3,12 +3,12 @@ secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" stack_name: "{{ aws_server_name | replace('.', '-') }}" -- name: Locate official Ubuntu 16.04 AMI for region +- name: Locate official AMI for region ec2_ami_find: aws_access_key: "{{ access_key }}" aws_secret_key: "{{ secret_key }}" - name: "ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-*" - owner: 099720109477 + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" + owner: "{{ cloud_providers.ec2.image.owner }}" sort: creationDate sort_order: descending sort_end: 1 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 08a380e4..4fbb6957 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -14,7 +14,7 @@ instance_names: "{{ server_name }}" zone: "{{ zone }}" machine_type: "{{ cloud_providers.gce.size }}" - image: ubuntu-1604 + image: "{{ cloud_providers.gce.image }}" service_account_email: "{{ service_account_email }}" credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index f2f0aeb3..40148b45 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -15,8 +15,6 @@ - include: freebsd.yml when: ansible_distribution == 'FreeBSD' -- meta: flush_handlers - - name: Dnsmasq configured template: src=dnsmasq.conf.j2 dest="{{ config_prefix|default('/') }}etc/dnsmasq.conf" notify: @@ -37,5 +35,7 @@ shell: > sudo -u dnsmasq "/usr/local/sbin/adblock.sh" +- meta: flush_handlers + - name: Dnsmasq enabled and started service: name=dnsmasq state=started enabled=yes diff --git a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 index cf4a1e4d..25a56373 100644 --- a/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 +++ b/roles/dns_adblocking/templates/usr.sbin.dnsmasq.j2 @@ -15,6 +15,8 @@ /etc/dnsmasq.d/* r, /var/lib/dnsmasq/ r, /var/lib/dnsmasq/block.hosts r, + /etc/dnsmasq.d-available/ r, + /etc/dnsmasq.d-available/* r, /usr/sbin/dnsmasq mr, diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 87856355..b64cc1b2 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -15,6 +15,11 @@ - name: Install strongSwan package: name=strongswan state=present +- name: Get StrongSwan versions + shell: > + ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" + register: strongswan_version + - include: ipec_configuration.yml - include: openssl.yml - include: distribute_keys.yml diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 9a325526..36dc3176 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -5,7 +5,11 @@ config setup conn %default fragmentation=yes rekey=no +{% if strongswan_version.stdout is defined and strongswan_version.stdout > '550' %} + dpdaction=clear +{% else %} dpdaction=none +{% endif %} keyexchange=ikev2 compress=yes dpddelay=35s From 2782df8cfd86cab52fe3d4b2b3698356ca4ab64a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 22 Apr 2017 23:09:37 +0200 Subject: [PATCH 482/769] Move back to 16.04. Forgot to change after testing --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index c63a46c8..fde48393 100644 --- a/config.cfg +++ b/config.cfg @@ -72,7 +72,7 @@ cloud_providers: ec2: size: t2.micro image: - name: "ubuntu-zesty-17.04" # ubuntu-xenial-16.04 / ubuntu-zesty-17.04 + name: "ubuntu-xenial-16.04" # ubuntu-xenial-16.04 / ubuntu-zesty-17.04 owner: "099720109477" gce: size: f1-micro From 8c430bd55555447fd0de7c71801faea397c60edc Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 22 Apr 2017 22:38:29 -0400 Subject: [PATCH 483/769] typo (#474) --- docs/cloud-unsupported.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cloud-unsupported.md b/docs/cloud-unsupported.md index 82cc2ce7..3e1e5dab 100644 --- a/docs/cloud-unsupported.md +++ b/docs/cloud-unsupported.md @@ -13,8 +13,8 @@ If you want Algo to officially support your new cloud provider then it must have Hosting providers that rely on OpenVZ or Docker cannot be used by Algo since they cannot load the required kernel modules or access the required network interfaces. For more information, see the strongSwan documentation on [Cloud Platforms](https://wiki.strongswan.org/projects/strongswan/wiki/Cloudplatforms). -In order to address this issue, strongSwan has developed the [kernel-libipsec](https://wiki.strongswan.org/projects/strongswan/wiki/Kernel-libipsec) plugin which provides an IPsec backend that works entirely in userland. `libipsec` bundles its own IPsec implementation and uses TUN devices to route packets. For example, `libipsec` is used by the Android strongSwan app to address Adnroid's lack of a functional IPsec stack. +In order to address this issue, strongSwan has developed the [kernel-libipsec](https://wiki.strongswan.org/projects/strongswan/wiki/Kernel-libipsec) plugin which provides an IPsec backend that works entirely in userland. `libipsec` bundles its own IPsec implementation and uses TUN devices to route packets. For example, `libipsec` is used by the Android strongSwan app to address Android's lack of a functional IPsec stack. -Use of `libipsec` is not supported by Algo. It has known performance issues since it buffers each packet in memory. On certain systems with insufficient processor power, such as many cloud hosting providers, using `libipsec` can lead to an out of memory conditions, crash the charon daemon, or lock up the entire host. +Use of `libipsec` is not supported by Algo. It has known performance issues since it buffers each packet in memory. On certain systems with insufficient processor power, such as many cloud hosting providers, using `libipsec` can lead to an out of memory condition, crash the charon daemon, or lock up the entire host. -Further, `libipsec` introduces unknown security risks. The code in `libipsec` has not been scrutinized to the same level as the code in the Linux or FreeBSD kernel that it replaces. This additional code introduces new complexity to the Algo server that we want to avoid at this time. We recommend moving to a hosting provider that does not require libipsec. \ No newline at end of file +Further, `libipsec` introduces unknown security risks. The code in `libipsec` has not been scrutinized to the same level as the code in the Linux or FreeBSD kernel that it replaces. This additional code introduces new complexity to the Algo server that we want to avoid at this time. We recommend moving to a hosting provider that does not require libipsec and can load the required kernel modules. From aac052da468af577db7028b4156f504d0b2391b4 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 23 Apr 2017 09:04:30 -0400 Subject: [PATCH 484/769] this option is deprecated (#477) --- roles/security/templates/sshd_config.j2 | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 index daddbed1..4bdb2601 100644 --- a/roles/security/templates/sshd_config.j2 +++ b/roles/security/templates/sshd_config.j2 @@ -25,7 +25,6 @@ AcceptEnv LANG LC_* # Turn off a lot of features IgnoreRhosts yes -RhostsRSAAuthentication no HostbasedAuthentication no PermitEmptyPasswords no ChallengeResponseAuthentication no From 0d1c760a6337ee811e3d8f16ca95982d271829c1 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 23 Apr 2017 14:54:54 -0400 Subject: [PATCH 485/769] Doc improvements (#479) * cleanup * typos * Closes #289 Add instructions for connecting to the VPN and configuring on demand. --- .travis.yml | 2 -- CONTRIBUTING.md | 4 ++-- README.md | 12 ++++++++---- docs/index.md | 8 ++++---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b304f7a..98ba83b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,8 +45,6 @@ install: - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt - pip install ansible-lint - -before_script: - gem install awesome_bot script: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 250dbbfa..fe17c839 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,9 @@ * Check that your issue is not already described in the [FAQ](docs/faq.md), [troubleshooting](docs/troubleshooting.md) docs, or an [existing issue](https://github.com/trailofbits/algo/issues) * Did you remember to install the dependencies for your operating system prior to installing Algo? -* We only support modern operating systems, e.g. macOS 10.11+, iOS 9+, Windows 10+, Ubuntu 17.04+, etc. +* We only support modern clients, e.g. macOS 10.11+, iOS 9+, Windows 10+, Ubuntu 17.04+, etc. * Cloud provider support is limited to DO, AWS, GCE, and Azure. Any others are best effort only. -* If you need to file a new issue, fill out any relevant fields in the Issue Template +* If you need to file a new issue, fill out any relevant fields in the Issue Template. ### Pull Requests diff --git a/README.md b/README.md index 86a74bea..9ba625c6 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,10 @@ Distribute the configuration files to your users, so they can connect to the VPN Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. +On iOS, you can connect to the VPN by opening Settings and clicking the toggle next to "VPN" near the top of the list. On macOS, you can connect to the VPN by opening System Preferences -> Network, finding Algo VPN in the left column and clicking "Connect." On macOS, we recommend checking "Show VPN status in menu bar" too which lets you connect and disconnect from the menu bar. + +If you enabled "On Demand", the VPN will connect automatically whenever it is able. On iOS, you can turn off "On Demand" by clicking the (i) next to the entry for Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "On Demand" by opening the Network Preferences, finding Algo VPN in the left column, and unchecking the box for "Connect on demand." + ### Android Devices You need to install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more detailed steps. @@ -199,7 +203,7 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation * Setup instructions - - Documentation for avaialble [Ansible roles](docs/deploy-with-ansible.md) + - Documentation for available [Ansible roles](docs/deploy-with-ansible.md) - Deploy from [RedHat/CentOS 6.x](docs/setup-redhat-centos6.md) * Client setup - Setup [Windows](docs/client-windows.md) clients @@ -209,9 +213,9 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. - Configure [Azure](docs/cloud-azure.md) - Deploy to an [unsupported cloud provider](docs/cloud-unsupported.md) * Advanced Deployment - - Deploy to local [FreeBSD](docs/deploy-to-freebsd.md) servers - - Deploy to local [Ubuntu 16.04](docs/deploy-to-ubuntu.md) servers - - Deploy with [Ansible](docs/deploy-with-ansible.md) + - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server + - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server + - Deploy with [Ansible](docs/deploy-with-ansible.md) directly * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) diff --git a/docs/index.md b/docs/index.md index d56e81b7..80d39652 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ # Algo VPN documentation * Setup instructions - - Documentation for avaialble [Ansible roles](setup-roles.md) + - Documentation for available [Ansible roles](setup-roles.md) - Deploy from [RedHat/CentOS 6.x](setup-redhat-centos6.md) * Client setup - Setup [Windows](client-windows.md) clients @@ -11,9 +11,9 @@ - Configure [Azure](cloud-azure.md) - Deploy to an [unsupported cloud provider](cloud-unsupported.md) * Advanced Deployment - - Deploy to local [FreeBSD](deploy-to-freebsd.md) servers - - Deploy to local [Ubuntu 16.04](deploy-to-ubuntu.md) servers - - Deploy with [Ansible](deploy-with-ansible.md) + - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server + - Deploy to your own [Ubuntu 16.04](deploy-to-ubuntu.md) server + - Deploy with [Ansible](deploy-with-ansible.md) directly * [FAQ](faq.md) * [Troubleshooting](troubleshooting.md) From 451394100db4cbb93d9d13345c54747e3146ccaa Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 23 Apr 2017 22:00:37 +0200 Subject: [PATCH 486/769] Some enhances in the compat ciphers (#464) raise the IntegrityCheckMethod to SHA384 Move Windows to ECDSA Increase IntegrityCheckMethod --- playbooks/facts/main.yml | 5 ----- roles/vpn/defaults/main.yml | 4 ++-- roles/vpn/templates/client_ipsec.secrets.j2 | 4 ---- roles/vpn/templates/client_windows.ps1.j2 | 2 +- roles/vpn/templates/ipsec.secrets.j2 | 4 ---- 5 files changed, 3 insertions(+), 16 deletions(-) diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index d66f15c4..7c8516d7 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -35,8 +35,3 @@ - name: Define the commonName set_fact: IP_subject_alt_name: "{{ IP_subject_alt_name }}" - -- name: Change the algorithm to RSA - set_fact: - algo_params: "rsa:2048" - when: Win10_Enabled is defined and Win10_Enabled == "Y" diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index d4e9bfd4..49f118d5 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -25,5 +25,5 @@ ciphers: ike: aes128gcm16-prfsha512-ecp256! esp: aes128gcm16-ecp256! compat: - ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! - esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_256-prfsha256-modp2048! + ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256! + esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256! diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/vpn/templates/client_ipsec.secrets.j2 index 0d8356ee..9feb9f51 100644 --- a/roles/vpn/templates/client_ipsec.secrets.j2 +++ b/roles/vpn/templates/client_ipsec.secrets.j2 @@ -1,5 +1 @@ -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} -{{ IP_subject_alt_name }} : RSA {{ item }}.key -{% else %} {{ IP_subject_alt_name }} : ECDSA {{ item }}.key -{% endif %} diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4df2297f..4eb87aa5 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,3 @@ certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup none diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/vpn/templates/ipsec.secrets.j2 index 2226f04e..27df8f6a 100644 --- a/roles/vpn/templates/ipsec.secrets.j2 +++ b/roles/vpn/templates/ipsec.secrets.j2 @@ -1,5 +1 @@ -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} -: RSA {{ IP_subject_alt_name }}.key -{% else %} : ECDSA {{ IP_subject_alt_name }}.key -{% endif %} From 31d6bd39a1a1da05e3e558c37c32c503ec6169a9 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 23 Apr 2017 16:36:30 -0400 Subject: [PATCH 487/769] The docs got out of sync with the scripts (#480) * The docs got out of sync with the scripts * restructure * fix links --- README.md | 12 ++++++------ ...deploy-with-ansible.md => deploy-from-ansible.md} | 0 ...dhat-centos6.md => deploy-from-redhat-centos6.md} | 0 docs/{client-windows.md => deploy-from-windows.md} | 0 ...unsupported.md => deploy-to-unsupported-cloud.md} | 0 docs/index.md | 8 ++++---- 6 files changed, 10 insertions(+), 10 deletions(-) rename docs/{deploy-with-ansible.md => deploy-from-ansible.md} (100%) rename docs/{setup-redhat-centos6.md => deploy-from-redhat-centos6.md} (100%) rename docs/{client-windows.md => deploy-from-windows.md} (100%) rename docs/{cloud-unsupported.md => deploy-to-unsupported-cloud.md} (100%) diff --git a/README.md b/README.md index 9ba625c6..26c46b0a 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Your VPN is now installed and ready to use. If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: ```powershell -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants SHA256128 -CipherTransformConstants AES256 -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 -DHGroup Group14 -PfsGroup none +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup none ``` ### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) @@ -203,19 +203,19 @@ The Algo VPN server now contains only the users listed in the `config.cfg` file. ## Additional Documentation * Setup instructions - - Documentation for available [Ansible roles](docs/deploy-with-ansible.md) - - Deploy from [RedHat/CentOS 6.x](docs/setup-redhat-centos6.md) + - Documentation for available [Ansible roles](docs/setup-roles.md) + - Deploy from [RedHat/CentOS 6.x](docs/deploy-from-redhat-centos6.md) + - Deploy from [Windows](docs/deploy-from-windows.md) + - Deploy from [Ansible](docs/deploy-from-ansible.md) directly * Client setup - - Setup [Windows](docs/client-windows.md) clients - Setup [Android](docs/client-android.md) clients - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](docs/cloud-azure.md) - - Deploy to an [unsupported cloud provider](docs/cloud-unsupported.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server - - Deploy with [Ansible](docs/deploy-with-ansible.md) directly + - Deploy to an [unsupported cloud provider](docs/deploy-to-unsupported-cloud.md) * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) diff --git a/docs/deploy-with-ansible.md b/docs/deploy-from-ansible.md similarity index 100% rename from docs/deploy-with-ansible.md rename to docs/deploy-from-ansible.md diff --git a/docs/setup-redhat-centos6.md b/docs/deploy-from-redhat-centos6.md similarity index 100% rename from docs/setup-redhat-centos6.md rename to docs/deploy-from-redhat-centos6.md diff --git a/docs/client-windows.md b/docs/deploy-from-windows.md similarity index 100% rename from docs/client-windows.md rename to docs/deploy-from-windows.md diff --git a/docs/cloud-unsupported.md b/docs/deploy-to-unsupported-cloud.md similarity index 100% rename from docs/cloud-unsupported.md rename to docs/deploy-to-unsupported-cloud.md diff --git a/docs/index.md b/docs/index.md index 80d39652..7275901f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,18 +2,18 @@ * Setup instructions - Documentation for available [Ansible roles](setup-roles.md) - - Deploy from [RedHat/CentOS 6.x](setup-redhat-centos6.md) + - Deploy from [RedHat/CentOS 6.x](deploy-from-redhat-centos6.md) + - Deploy from [Windows](deploy-from-windows.md) + - Deploy from [Ansible](deploy-from-ansible.md) directly * Client setup - - Setup [Windows](client-windows.md) clients - Setup [Android](client-android.md) clients - Setup [Generic/Linux](client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](cloud-azure.md) - - Deploy to an [unsupported cloud provider](cloud-unsupported.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 16.04](deploy-to-ubuntu.md) server - - Deploy with [Ansible](deploy-with-ansible.md) directly + - Deploy to an [unsupported cloud provider](deploy-to-unsupported-cloud.md) * [FAQ](faq.md) * [Troubleshooting](troubleshooting.md) From aea22475c3a61d1270d9255d4bf9585b64823581 Mon Sep 17 00:00:00 2001 From: Nicholas Date: Mon, 24 Apr 2017 10:53:58 -0400 Subject: [PATCH 488/769] Fixed broken links to ansible deployment instructions (#484) * Fixed broken link in EC2 IAM instructions * Fixed broken in step 6 of instructions --- README.md | 2 +- algo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26c46b0a..9fcad771 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-with-ansible.md](docs/deploy-with-ansible.md). +6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-from-ansible.md](docs/deploy-from-ansible.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. diff --git a/algo b/algo index 0ca82bd7..901bc0f4 100755 --- a/algo +++ b/algo @@ -232,7 +232,7 @@ EXTRA_VARS="do_access_token=$do_access_token do_server_name=$do_server_name do_r ec2 () { read -p " Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-with-ansible.md). +Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). $ADDITIONAL_PROMPT [AKIA...]: " -rs aws_access_key From 540c761d3b4fdd865568b7438e257d1e9ee8ebac Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 25 Apr 2017 23:06:51 +0200 Subject: [PATCH 489/769] Disable RSA in the mobileconfigs. Fixes #486 --- roles/vpn/templates/mobileconfig.j2 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 6d54233c..1a892d8f 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -94,11 +94,7 @@ PayloadCertificateUUID {{ pkcs12_PayloadCertificateUUID }} CertificateType -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} - RSA2048 -{% else %} ECDSA256 -{% endif %} ServerCertificateIssuerCommonName {{ IP_subject_alt_name }} RemoteAddress From f002f32836a0deffaf3694e81057609520ec0684 Mon Sep 17 00:00:00 2001 From: forkbomber Date: Thu, 27 Apr 2017 18:46:28 +0200 Subject: [PATCH 490/769] Fix typo related to "Error 809" and filtered IKE_AUTH requests (#496) --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 3ce2d768..b22efae4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -178,7 +178,7 @@ $ sudo ifconfig wlan0 mtu 1438 On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. -It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with 'Block Fragmented IP packets'. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). +It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). ## I have a problem not covered here From 0cb43650cb656749fa3872da0631d22f52d3c4bf Mon Sep 17 00:00:00 2001 From: Ryan Kasper Date: Thu, 27 Apr 2017 10:46:50 -0600 Subject: [PATCH 491/769] Windows 10 -PfsGroup None --> -PfsGroup ECP256 (#493) * Windows 10 -PfsGroup None --> -PfsGroup ECP256 Fixes broken tunnel when rekey (CREATE_CHILD_SA request [ N(REKEY_SA) SA No TSi TSr KE ]) occurs (on my Windows 10 1703 build 15063.138 Creator's Update system this is ~every 57 minutes) * Update Windows Client PfsGroup Commandline --- README.md | 2 +- roles/vpn/templates/client_windows.ps1.j2 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fcad771..eefd0447 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Your VPN is now installed and ready to use. If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: ```powershell -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup none +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 ``` ### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4eb87aa5..78201de2 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,3 @@ certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup none +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 From 0ed68b6c303ef337c07abd98b7956e1b6c5da6e7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 27 Apr 2017 18:47:05 +0200 Subject: [PATCH 492/769] Properly configure ICMP restrictions (#492) --- roles/vpn/templates/rules.v4.j2 | 3 +-- roles/vpn/templates/rules.v6.j2 | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 5ced4eeb..e040b184 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -22,7 +22,7 @@ COMMIT -A INPUT -p esp -j ACCEPT -A INPUT -p ah -j ACCEPT # rate limit ICMP traffic per source --A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP +-A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT @@ -41,4 +41,3 @@ COMMIT -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT COMMIT - diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index f8d9593f..640f6d29 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -24,7 +24,7 @@ COMMIT -A INPUT -p esp -j ACCEPT -A INPUT -m ah -j ACCEPT # rate limit ICMP traffic per source --A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j DROP +-A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT @@ -57,4 +57,3 @@ COMMIT -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " -A ICMPV6-CHECK-LOG -j DROP COMMIT - From 2f5c050fd2e759bc0cffb1742c98103c365f5b91 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 27 Apr 2017 20:47:45 +0200 Subject: [PATCH 493/769] dpdaction to clear (#498) --- roles/vpn/templates/ipsec.conf.j2 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 36dc3176..313d6897 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -5,11 +5,7 @@ config setup conn %default fragmentation=yes rekey=no -{% if strongswan_version.stdout is defined and strongswan_version.stdout > '550' %} dpdaction=clear -{% else %} - dpdaction=none -{% endif %} keyexchange=ikev2 compress=yes dpddelay=35s From bd348af9c29d748739e7ade62ea7f058383a5c5f Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sat, 29 Apr 2017 16:48:25 +0200 Subject: [PATCH 494/769] Implementing blocks and additional fail hints #487 (#497) change the troubleshooting url --- config.cfg | 5 + deploy.yml | 94 ++++--- roles/cloud-azure/tasks/main.yml | 255 +++++++++--------- roles/cloud-digitalocean/tasks/main.yml | 194 ++++++------- roles/cloud-ec2/tasks/main.yml | 114 ++++---- roles/cloud-gce/tasks/main.yml | 118 ++++---- roles/common/tasks/main.yml | 46 ++-- roles/dns_adblocking/tasks/main.yml | 65 ++--- roles/local/tasks/main.yml | 69 ++--- roles/security/tasks/main.yml | 159 +++++------ roles/ssh_tunneling/tasks/main.yml | 139 +++++----- roles/vpn/tasks/main.yml | 49 ++-- users.yml | 345 +++++++++++++----------- 13 files changed, 877 insertions(+), 775 deletions(-) diff --git a/config.cfg b/config.cfg index fde48393..78407bae 100644 --- a/config.cfg +++ b/config.cfg @@ -78,3 +78,8 @@ cloud_providers: size: f1-micro image: ubuntu-1604 # ubuntu-1604 / ubuntu-1704 local: + +fail_hint: + - Sorry, but something went wrong! + - Please check the troubleshooting guide. + - https://trailofbits.github.io/algo/troubleshooting.html diff --git a/deploy.yml b/deploy.yml index 5623c253..91721c11 100644 --- a/deploy.yml +++ b/deploy.yml @@ -5,15 +5,21 @@ - config.cfg pre_tasks: - - name: Local pre-tasks - include: playbooks/local.yml - tags: [ 'always' ] + - block: + - name: Local pre-tasks + include: playbooks/local.yml + tags: [ 'always' ] - - name: Local pre-tasks - include: playbooks/local_ssh.yml - become: false - when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" - tags: [ 'local' ] + - name: Local pre-tasks + include: playbooks/local_ssh.yml + become: false + when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" + tags: [ 'local' ] + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always roles: - { role: cloud-digitalocean, tags: ['digitalocean'] } @@ -23,10 +29,16 @@ - { role: local, tags: ['local'] } post_tasks: - - name: Local post-tasks - include: playbooks/post.yml - become: false - tags: [ 'cloud' ] + - block: + - name: Local post-tasks + include: playbooks/post.yml + become: false + tags: [ 'cloud' ] + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always - name: Configure the server and install required software hosts: vpn-host @@ -37,9 +49,15 @@ - config.cfg pre_tasks: - - name: Common pre-tasks - include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] + - block: + - name: Common pre-tasks + include: playbooks/common.yml + tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always roles: - { role: security, tags: [ 'security' ] } @@ -48,25 +66,31 @@ - { role: vpn, tags: [ 'vpn' ] } post_tasks: - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - - " {% if Store_CAKEY is defined and Store_CAKEY == 'N' %}{% else %}{{ congrats.ca_key_pass }}{% endif %}" - - " {% if cloud_deployment is defined %}{{ congrats.ssh_access }}{% endif %}" - tags: always + - block: + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" + - " {% if Store_CAKEY is defined and Store_CAKEY == 'N' %}{% else %}{{ congrats.ca_key_pass }}{% endif %}" + - " {% if cloud_deployment is defined %}{{ congrats.ssh_access }}{% endif %}" + tags: always - - name: Save the CA key password - local_action: > - shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password - become: no - tags: tests + - name: Save the CA key password + local_action: > + shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password + become: no + tags: tests - - name: Delete the CA key - local_action: - module: file - path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" - state: absent - become: no - tags: always - when: Store_CAKEY is defined and Store_CAKEY == "N" + - name: Delete the CA key + local_action: + module: file + path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + state: absent + become: no + tags: always + when: Store_CAKEY is defined and Store_CAKEY == "N" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 9048615f..4cf621fa 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,138 +1,143 @@ --- +- block: + - set_fact: + resource_group: "Algo_{{ region }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" -- set_fact: - resource_group: "Algo_{{ region }}" - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + - name: Create a resource group + azure_rm_resourcegroup: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + name: "{{ resource_group }}" + location: "{{ region }}" + tags: + Environment: Algo -- name: Create a resource group - azure_rm_resourcegroup: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - name: "{{ resource_group }}" - location: "{{ region }}" - tags: - Environment: Algo + - name: Create a virtual network + azure_rm_virtualnetwork: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group: "{{ resource_group }}" + name: algo_net + address_prefixes: "10.10.0.0/16" + tags: + Environment: Algo -- name: Create a virtual network - azure_rm_virtualnetwork: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_net - address_prefixes: "10.10.0.0/16" - tags: - Environment: Algo + - name: Create a security group + azure_rm_securitygroup: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group: "{{ resource_group }}" + name: AlgoSecGroup + purge_rules: yes + rules: + - name: AllowSSH + protocol: Tcp + destination_port_range: 22 + access: Allow + priority: 100 + direction: Inbound + - name: AllowIPSEC500 + protocol: Udp + destination_port_range: 500 + access: Allow + priority: 110 + direction: Inbound + - name: AllowIPSEC4500 + protocol: Udp + destination_port_range: 4500 + access: Allow + priority: 120 + direction: Inbound -- name: Create a security group - azure_rm_securitygroup: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: AlgoSecGroup - purge_rules: yes - rules: - - name: AllowSSH - protocol: Tcp - destination_port_range: 22 - access: Allow - priority: 100 - direction: Inbound - - name: AllowIPSEC500 - protocol: Udp - destination_port_range: 500 - access: Allow - priority: 110 - direction: Inbound - - name: AllowIPSEC4500 - protocol: Udp - destination_port_range: 4500 - access: Allow - priority: 120 - direction: Inbound + - name: Create a subnet + azure_rm_subnet: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group: "{{ resource_group }}" + name: algo_subnet + address_prefix: "10.10.0.0/24" + virtual_network: algo_net + security_group_name: AlgoSecGroup + tags: + Environment: Algo -- name: Create a subnet - azure_rm_subnet: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_subnet - address_prefix: "10.10.0.0/24" - virtual_network: algo_net - security_group_name: AlgoSecGroup - tags: - Environment: Algo + - name: Create an instance + azure_rm_virtualmachine: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group: "{{ resource_group }}" + admin_username: ubuntu + virtual_network: algo_net + name: "{{ azure_server_name }}" + ssh_password_enabled: false + vm_size: "{{ cloud_providers.azure.size }}" + tags: + Environment: Algo + ssh_public_keys: + - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } + image: "{{ cloud_providers.azure.image }}" + register: azure_rm_virtualmachine -- name: Create an instance - azure_rm_virtualmachine: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - admin_username: ubuntu - virtual_network: algo_net - name: "{{ azure_server_name }}" - ssh_password_enabled: false - vm_size: "{{ cloud_providers.azure.size }}" - tags: - Environment: Algo - ssh_public_keys: - - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } - image: "{{ cloud_providers.azure.image }}" - register: azure_rm_virtualmachine + # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? - # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? + - set_fact: + ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" + networkinterface_name: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].name }}" -- set_fact: - ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" - networkinterface_name: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].name }}" + - name: Ensure the network interface includes all required parameters + azure_rm_networkinterface: + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + name: "{{ networkinterface_name }}" + resource_group: "{{ resource_group }}" + virtual_network_name: algo_net + subnet_name: algo_subnet + security_group_name: AlgoSecGroup -- name: Ensure the network interface includes all required parameters - azure_rm_networkinterface: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - name: "{{ networkinterface_name }}" - resource_group: "{{ resource_group }}" - virtual_network_name: algo_net - subnet_name: algo_subnet - security_group_name: AlgoSecGroup + - name: Add the instance to an inventory group + add_host: + name: "{{ ip_address }}" + groups: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: azure + ipv6_support: no -- name: Add the instance to an inventory group - add_host: - name: "{{ ip_address }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: azure - ipv6_support: no + - set_fact: + cloud_instance_ip: "{{ ip_address }}" -- set_fact: - cloud_instance_ip: "{{ ip_address }}" + - name: Ensure the group azure exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[azure]' -- name: Ensure the group azure exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[azure]' - -- name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[azure\]' - regexp: "^{{ cloud_instance_ip }}.*" - line: "{{ cloud_instance_ip }}" + - name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[azure\]' + regexp: "^{{ cloud_instance_ip }}.*" + line: "{{ cloud_instance_ip }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 897f52af..66308423 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,102 +1,108 @@ -- name: Set the DigitalOcean Access Token fact - set_fact: - do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN'), true) }}" - public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - - block: - - name: "Delete the existing Algo SSH keys" + - name: Set the DigitalOcean Access Token fact + set_fact: + do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN'), true) }}" + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + + - block: + - name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 + + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + + - fail: + msg: "Please, ensure that your API token is not read-only." + + - name: "Upload the SSH key" digital_ocean: - state: absent + state: present command: ssh + ssh_pub_key: "{{ public_key }}" api_token: "{{ do_token }}" name: "{{ SSH_keys.comment }}" - register: ssh_keys - until: ssh_keys.changed != true - retries: 10 - delay: 1 + register: do_ssh_key + - name: "Creating a droplet..." + digital_ocean: + state: present + command: droplet + name: "{{ do_server_name }}" + region_id: "{{ do_region }}" + size_id: "{{ cloud_providers.digitalocean.size }}" + image_id: "{{ cloud_providers.digitalocean.image }}" + ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" + unique_name: yes + api_token: "{{ do_token }}" + ipv6: yes + register: do + + - name: Add the droplet to an inventory group + add_host: + name: "{{ do.droplet.ip_address }}" + groups: vpn-host + ansible_ssh_user: root + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + do_access_token: "{{ do_token }}" + do_droplet_id: "{{ do.droplet.id }}" + cloud_provider: digitalocean + ipv6_support: true + + - set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" + + - name: Tag the droplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ do_token }}" + state: present + + - name: Get droplets + uri: + url: "https://api.digitalocean.com/v2/droplets?tag_name=Environment:Algo" + method: GET + status_code: 200 + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ do_token }}" + register: do_droplets + + - name: Ensure the group digitalocean exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[digitalocean]' + + - name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[digitalocean\]' + regexp: "^{{ item.networks.v4[0].ip_address }}.*" + line: "{{ item.networks.v4[0].ip_address }}" + with_items: + - "{{ do_droplets.json.droplets }}" rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes - - - debug: var=ssh_keys - + - debug: var=fail_hint + tags: always - fail: - msg: "Please, ensure that your API token is not read-only." - -- name: "Upload the SSH key" - digital_ocean: - state: present - command: ssh - ssh_pub_key: "{{ public_key }}" - api_token: "{{ do_token }}" - name: "{{ SSH_keys.comment }}" - register: do_ssh_key - -- name: "Creating a droplet..." - digital_ocean: - state: present - command: droplet - name: "{{ do_server_name }}" - region_id: "{{ do_region }}" - size_id: "{{ cloud_providers.digitalocean.size }}" - image_id: "{{ cloud_providers.digitalocean.image }}" - ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" - unique_name: yes - api_token: "{{ do_token }}" - ipv6: yes - register: do - -- name: Add the droplet to an inventory group - add_host: - name: "{{ do.droplet.ip_address }}" - groups: vpn-host - ansible_ssh_user: root - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - do_access_token: "{{ do_token }}" - do_droplet_id: "{{ do.droplet.id }}" - cloud_provider: digitalocean - ipv6_support: true - -- set_fact: - cloud_instance_ip: "{{ do.droplet.ip_address }}" - -- name: Tag the droplet - digital_ocean_tag: - name: "Environment:Algo" - resource_id: "{{ do.droplet.id }}" - api_token: "{{ do_token }}" - state: present - -- name: Get droplets - uri: - url: "https://api.digitalocean.com/v2/droplets?tag_name=Environment:Algo" - method: GET - status_code: 200 - headers: - Content-Type: "application/json" - Authorization: "Bearer {{ do_token }}" - register: do_droplets - -- name: Ensure the group digitalocean exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[digitalocean]' - -- name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[digitalocean\]' - regexp: "^{{ item.networks.v4[0].ip_address }}.*" - line: "{{ item.networks.v4[0].ip_address }}" - with_items: - - "{{ do_droplets.json.droplets }}" + tags: always diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index b99255c9..6c397a52 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,63 +1,69 @@ -- set_fact: - access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" - secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" - stack_name: "{{ aws_server_name | replace('.', '-') }}" +- block: + - set_fact: + access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + stack_name: "{{ aws_server_name | replace('.', '-') }}" -- name: Locate official AMI for region - ec2_ami_find: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" - owner: "{{ cloud_providers.ec2.image.owner }}" - sort: creationDate - sort_order: descending - sort_end: 1 - region: "{{ region }}" - register: ami_search + - name: Locate official AMI for region + ec2_ami_find: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" + owner: "{{ cloud_providers.ec2.image.owner }}" + sort: creationDate + sort_order: descending + sort_end: 1 + region: "{{ region }}" + register: ami_search -- set_fact: - ami_image: "{{ ami_search.results[0].ami_id }}" + - set_fact: + ami_image: "{{ ami_search.results[0].ami_id }}" -- include: encrypt_image.yml - tags: [encrypted] + - include: encrypt_image.yml + tags: [encrypted] -- include: cloudformation.yml + - include: cloudformation.yml -- name: Add new instance to host group - add_host: - hostname: "{{ stack.stack_outputs.PublicIP }}" - groupname: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: ec2 - ipv6_support: yes + - name: Add new instance to host group + add_host: + hostname: "{{ stack.stack_outputs.PublicIP }}" + groupname: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: ec2 + ipv6_support: yes -- set_fact: - cloud_instance_ip: "{{ stack.stack_outputs.PublicIP }}" + - set_fact: + cloud_instance_ip: "{{ stack.stack_outputs.PublicIP }}" -- name: Get EC2 instances - ec2_remote_facts: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - region: "{{ region }}" - filters: - instance-state-name: running - "tag:Environment": Algo - register: algo_instances + - name: Get EC2 instances + ec2_remote_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + region: "{{ region }}" + filters: + instance-state-name: running + "tag:Environment": Algo + register: algo_instances -- name: Ensure the group ec2 exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[ec2]' + - name: Ensure the group ec2 exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[ec2]' -- name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[ec2\]' - regexp: "^{{ item.public_ip_address }}.*" - line: "{{ item.public_ip_address }}" - with_items: - - "{{ algo_instances.instances }}" + - name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[ec2\]' + regexp: "^{{ item.public_ip_address }}.*" + line: "{{ item.public_ip_address }}" + with_items: + - "{{ algo_instances.instances }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 4fbb6957..bbd9ef2e 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,64 +1,70 @@ -- set_fact: - credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" - ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" +- block: + - set_fact: + credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" -- set_fact: - credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" + - set_fact: + credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" -- set_fact: - service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" - project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + - set_fact: + service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" + project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" -- name: "Creating a new instance..." - gce: - instance_names: "{{ server_name }}" - zone: "{{ zone }}" - machine_type: "{{ cloud_providers.gce.size }}" - image: "{{ cloud_providers.gce.image }}" - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" - metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - # ip_forward: true - tags: - - "environment-algo" - register: google_vm + - name: "Creating a new instance..." + gce: + instance_names: "{{ server_name }}" + zone: "{{ zone }}" + machine_type: "{{ cloud_providers.gce.size }}" + image: "{{ cloud_providers.gce.image }}" + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' + # ip_forward: true + tags: + - "environment-algo" + register: google_vm -- name: Add the instance to an inventory group - add_host: - name: "{{ google_vm.instance_data[0].public_ip }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: gce - ipv6_support: no + - name: Add the instance to an inventory group + add_host: + name: "{{ google_vm.instance_data[0].public_ip }}" + groups: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: gce + ipv6_support: no -- name: Firewall configured - local_action: - module: gce_net - name: "{{ google_vm.instance_data[0].network }}" - fwname: "algo-ikev2" - allowed: "udp:500,4500;tcp:22" - state: "present" - src_range: 0.0.0.0/0 - service_account_email: "{{ credentials_file_lookup.client_email }}" - credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + - name: Firewall configured + local_action: + module: gce_net + name: "{{ google_vm.instance_data[0].network }}" + fwname: "algo-ikev2" + allowed: "udp:500,4500;tcp:22" + state: "present" + src_range: 0.0.0.0/0 + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ credentials_file_lookup.project_id }}" -- set_fact: - cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" + - set_fact: + cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" -- name: Ensure the group gce exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[gce]' + - name: Ensure the group gce exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[gce]' -- name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[gce\]' - regexp: "^{{ google_vm.instance_data[0].public_ip }}.*" - line: "{{ google_vm.instance_data[0].public_ip }}" + - name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[gce\]' + regexp: "^{{ google_vm.instance_data[0].public_ip }}.*" + line: "{{ google_vm.instance_data[0].public_ip }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 8c8a9933..781930e2 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,28 +1,28 @@ --- +- block: + - include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- name: Gather Facts - setup: - tags: - - always + - include: freebsd.yml + when: ansible_distribution == 'FreeBSD' -- include: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - name: Install tools + package: name="{{ item }}" state=present + with_items: + - "{{ tools|default([]) }}" + tags: + - always -- include: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - name: Sysctl tuning + sysctl: name="{{ item.item }}" value="{{ item.value }}" + with_items: + - "{{ sysctl|default([]) }}" + tags: + - always -- name: Install tools - package: name="{{ item }}" state=present - with_items: - - "{{ tools|default([]) }}" - tags: - - always - -- name: Sysctl tuning - sysctl: name="{{ item.item }}" value="{{ item.value }}" - with_items: - - "{{ sysctl|default([]) }}" - tags: - - always - -- meta: flush_handlers + - meta: flush_handlers + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 40148b45..f336d23e 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,41 +1,46 @@ --- +- block: + - name: Dnsmasq installed + package: name=dnsmasq -- name: Dnsmasq installed - package: name=dnsmasq + - name: Ensure that the dnsmasq user exist + user: name=dnsmasq groups=nogroup append=yes state=present -- name: Ensure that the dnsmasq user exist - user: name=dnsmasq groups=nogroup append=yes state=present + - name: The dnsmasq directory created + file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup -- name: The dnsmasq directory created - file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup + - include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- include: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - include: freebsd.yml + when: ansible_distribution == 'FreeBSD' -- include: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - name: Dnsmasq configured + template: src=dnsmasq.conf.j2 dest="{{ config_prefix|default('/') }}etc/dnsmasq.conf" + notify: + - restart dnsmasq -- name: Dnsmasq configured - template: src=dnsmasq.conf.j2 dest="{{ config_prefix|default('/') }}etc/dnsmasq.conf" - notify: - - restart dnsmasq + - name: Adblock script created + template: src=adblock.sh dest=/usr/local/sbin/adblock.sh owner=root group="{{ root_group|default('root') }}" mode=0755 -- name: Adblock script created - template: src=adblock.sh dest=/usr/local/sbin/adblock.sh owner=root group="{{ root_group|default('root') }}" mode=0755 + - name: Adblock script added to cron + cron: + name: Adblock hosts update + minute: 10 + hour: 2 + job: /usr/local/sbin/adblock.sh + user: dnsmasq -- name: Adblock script added to cron - cron: - name: Adblock hosts update - minute: 10 - hour: 2 - job: /usr/local/sbin/adblock.sh - user: dnsmasq + - name: Update adblock hosts + shell: > + sudo -u dnsmasq "/usr/local/sbin/adblock.sh" -- name: Update adblock hosts - shell: > - sudo -u dnsmasq "/usr/local/sbin/adblock.sh" + - meta: flush_handlers -- meta: flush_handlers - -- name: Dnsmasq enabled and started - service: name=dnsmasq state=started enabled=yes + - name: Dnsmasq enabled and started + service: name=dnsmasq state=started enabled=yes + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index 9b34d7c6..555baa45 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -1,35 +1,42 @@ -- name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - cloud_provider: local - when: server_ip != "localhost" +--- +- block: + - name: Add the instance to an inventory group + add_host: + name: "{{ server_ip }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + cloud_provider: local + when: server_ip != "localhost" -- name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_connection: local - cloud_provider: local - when: server_ip == "localhost" + - name: Add the instance to an inventory group + add_host: + name: "{{ server_ip }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_connection: local + cloud_provider: local + when: server_ip == "localhost" -- set_fact: - cloud_instance_ip: "{{ server_ip }}" + - set_fact: + cloud_instance_ip: "{{ server_ip }}" -- name: Ensure the group local exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[local]' + - name: Ensure the group local exists in the dynamic inventory file + lineinfile: + state: present + dest: configs/inventory.dynamic + line: '[local]' -- name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[local\]' - regexp: "^{{ server_ip }}.*" - line: "{{ server_ip }}" + - name: Populate the dynamic inventory + lineinfile: + state: present + dest: configs/inventory.dynamic + insertafter: '\[local\]' + regexp: "^{{ server_ip }}.*" + line: "{{ server_ip }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 3ccef364..657e0c19 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -1,96 +1,101 @@ --- +- block: + - name: Install tools + apt: name="{{ item }}" state=latest + with_items: + - unattended-upgrades -- name: Install tools - apt: name="{{ item }}" state=latest - with_items: - - unattended-upgrades + - name: Configure unattended-upgrades + template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 -- name: Configure unattended-upgrades - template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 + - name: Periodic upgrades configured + template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 -- name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 + - name: Find directories for minimizing access + stat: + path: "{{ item }}" + register: minimize_access_directories + with_items: + - '/usr/local/sbin' + - '/usr/local/bin' + - '/usr/sbin' + - '/usr/bin' + - '/sbin' + - '/bin' -- name: Find directories for minimizing access - stat: - path: "{{ item }}" - register: minimize_access_directories - with_items: - - '/usr/local/sbin' - - '/usr/local/bin' - - '/usr/sbin' - - '/usr/bin' - - '/sbin' - - '/bin' + - name: Minimize access + file: path='{{ item.stat.path }}' mode='go-w' recurse=yes + when: item.stat.isdir + with_items: "{{ minimize_access_directories.results }}" + no_log: True -- name: Minimize access - file: path='{{ item.stat.path }}' mode='go-w' recurse=yes - when: item.stat.isdir - with_items: "{{ minimize_access_directories.results }}" - no_log: True + - name: Change shadow ownership to root and mode to 0600 + file: dest='/etc/shadow' owner=root group=root mode=0600 -- name: Change shadow ownership to root and mode to 0600 - file: dest='/etc/shadow' owner=root group=root mode=0600 + - name: change su-binary to only be accessible to user and group root + file: dest='/bin/su' owner=root group=root mode=0750 -- name: change su-binary to only be accessible to user and group root - file: dest='/bin/su' owner=root group=root mode=0750 + - name: Collect Use of privileged commands + shell: > + /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' + args: + executable: /bin/bash + register: privileged_programs -- name: Collect Use of privileged commands - shell: > - /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' - args: - executable: /bin/bash - register: privileged_programs + # Core dumps -# Core dumps + - name: Restrict core dumps (with PAM) + lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present -- name: Restrict core dumps (with PAM) - lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present + - name: Restrict core dumps (with sysctl) + sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present -- name: Restrict core dumps (with sysctl) - sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + # Kernel fixes -# Kernel fixes + - name: Disable Source Routed Packet Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_source_route + - net.ipv4.conf.default.accept_source_route + notify: + - flush routing cache -- name: Disable Source Routed Packet Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_source_route - - net.ipv4.conf.default.accept_source_route - notify: - - flush routing cache + - name: Disable ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.accept_redirects + - net.ipv4.conf.default.accept_redirects -- name: Disable ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects + - name: Disable Secure ICMP Redirect Acceptance + sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.secure_redirects + - net.ipv4.conf.default.secure_redirects + notify: + - flush routing cache -- name: Disable Secure ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.secure_redirects - - net.ipv4.conf.default.secure_redirects - notify: - - flush routing cache + - name: Enable Bad Error Message Protection + sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + notify: + - flush routing cache -- name: Enable Bad Error Message Protection - sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - notify: - - flush routing cache + - name: Enable RFC-recommended Source Route Validation + sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + with_items: + - net.ipv4.conf.all.rp_filter + - net.ipv4.conf.default.rp_filter + notify: + - flush routing cache -- name: Enable RFC-recommended Source Route Validation - sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present - with_items: - - net.ipv4.conf.all.rp_filter - - net.ipv4.conf.default.rp_filter - notify: - - flush routing cache + - name: Do not send ICMP redirects (we are not a router) + sysctl: name=net.ipv4.conf.all.send_redirects value=0 -- name: Do not send ICMP redirects (we are not a router) - sysctl: name=net.ipv4.conf.all.send_redirects value=0 - -- name: SSH config - template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 - notify: - - restart ssh + - name: SSH config + template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 + notify: + - restart ssh + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 5e1f2739..8d1b3a3e 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -1,77 +1,82 @@ --- +- block: + - name: Ensure that the sshd_config file has desired options + blockinfile: + dest: /etc/ssh/sshd_config + marker: '# {mark} ANSIBLE MANAGED BLOCK ssh_tunneling_role' + block: | + Match Group algo + AllowTcpForwarding local + AllowAgentForwarding no + AllowStreamLocalForwarding no + PermitTunnel no + X11Forwarding no + notify: + - restart ssh -- name: Ensure that the sshd_config file has desired options - blockinfile: - dest: /etc/ssh/sshd_config - marker: '# {mark} ANSIBLE MANAGED BLOCK ssh_tunneling_role' - block: | - Match Group algo - AllowTcpForwarding local - AllowAgentForwarding no - AllowStreamLocalForwarding no - PermitTunnel no - X11Forwarding no - notify: - - restart ssh + - name: Ensure that the algo group exist + group: name=algo state=present -- name: Ensure that the algo group exist - group: name=algo state=present + - name: Ensure that the jail directory exist + file: path=/var/jail/ state=directory mode=0755 owner=root group="{{ root_group|default('root') }}" -- name: Ensure that the jail directory exist - file: path=/var/jail/ state=directory mode=0755 owner=root group="{{ root_group|default('root') }}" + - name: Ensure that the SSH users exist + user: + name: "{{ item }}" + groups: algo + home: '/var/jail/{{ item }}' + createhome: yes + generate_ssh_key: yes + shell: /bin/false + ssh_key_type: ecdsa + ssh_key_bits: 256 + ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' + ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" + state: present + append: yes + with_items: "{{ users }}" -- name: Ensure that the SSH users exist - user: - name: "{{ item }}" - groups: algo - home: '/var/jail/{{ item }}' - createhome: yes - generate_ssh_key: yes - shell: /bin/false - ssh_key_type: ecdsa - ssh_key_bits: 256 - ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" - state: present - append: yes - with_items: "{{ users }}" + - name: The authorized keys file created + file: + src: '/var/jail/{{ item }}/.ssh/id_ecdsa.pub' + dest: '/var/jail/{{ item }}/.ssh/authorized_keys' + owner: "{{ item }}" + group: "{{ item }}" + state: link + with_items: "{{ users }}" -- name: The authorized keys file created - file: - src: '/var/jail/{{ item }}/.ssh/id_ecdsa.pub' - dest: '/var/jail/{{ item }}/.ssh/authorized_keys' - owner: "{{ item }}" - group: "{{ item }}" - state: link - with_items: "{{ users }}" + - name: Generate SSH fingerprints + shell: > + ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null + register: ssh_fingerprints -- name: Generate SSH fingerprints - shell: > - ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null - register: ssh_fingerprints + - name: Fetch users SSH private keys + fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat=yes + with_items: "{{ users }}" -- name: Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat=yes - with_items: "{{ users }}" + - name: Change mode for SSH private keys + local_action: file path=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem mode=0600 + with_items: "{{ users }}" + become: false -- name: Change mode for SSH private keys - local_action: file path=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem mode=0600 - with_items: "{{ users }}" - become: false + - name: Fetch the known_hosts file + local_action: + module: template + src: known_hosts.j2 + dest: configs/{{ IP_subject_alt_name }}/known_hosts + become: no -- name: Fetch the known_hosts file - local_action: - module: template - src: known_hosts.j2 - dest: configs/{{ IP_subject_alt_name }}/known_hosts - become: no - -- name: Build the client ssh config - local_action: - module: template - src: ssh_config.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config - mode: 0600 - become: no - with_items: - - "{{ users }}" + - name: Build the client ssh config + local_action: + module: template + src: ssh_config.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config + mode: 0600 + become: no + with_items: + - "{{ users }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index b64cc1b2..5e26ac59 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,31 +1,36 @@ --- +- block: + - name: Ensure that the strongswan group exist + group: name=strongswan state=present -- name: Ensure that the strongswan group exist - group: name=strongswan state=present + - name: Ensure that the strongswan user exist + user: name=strongswan group=strongswan state=present -- name: Ensure that the strongswan user exist - user: name=strongswan group=strongswan state=present + - include: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' -- include: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - include: freebsd.yml + when: ansible_distribution == 'FreeBSD' -- include: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - name: Install strongSwan + package: name=strongswan state=present -- name: Install strongSwan - package: name=strongswan state=present + - name: Get StrongSwan versions + shell: > + ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" + register: strongswan_version -- name: Get StrongSwan versions - shell: > - ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" - register: strongswan_version + - include: ipec_configuration.yml + - include: openssl.yml + - include: distribute_keys.yml + - include: client_configs.yml -- include: ipec_configuration.yml -- include: openssl.yml -- include: distribute_keys.yml -- include: client_configs.yml + - meta: flush_handlers -- meta: flush_handlers - -- name: strongSwan started - service: name=strongswan state=started + - name: strongSwan started + service: name=strongswan state=started + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/users.yml b/users.yml index d6e9399a..5f55bc0d 100644 --- a/users.yml +++ b/users.yml @@ -6,27 +6,33 @@ - config.cfg tasks: - - name: Add the server to the vpn-host group - add_host: - hostname: "{{ server_ip }}" - groupname: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - easyrsa_CA_password: "{{ easyrsa_CA_password }}" - IP_subject: "{{ IP_subject }}" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + - block: + - name: Add the server to the vpn-host group + add_host: + hostname: "{{ server_ip }}" + groupname: vpn-host + ansible_ssh_user: "{{ server_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" + easyrsa_CA_password: "{{ easyrsa_CA_password }}" + IP_subject: "{{ IP_subject }}" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - - name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ server_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present - become: false + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ server_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present + become: false + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always - name: User management hosts: vpn-host @@ -37,171 +43,188 @@ - roles/vpn/defaults/main.yml pre_tasks: - - name: Common pre-tasks - include: playbooks/common.yml + - block: + - name: Common pre-tasks + include: playbooks/common.yml + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always roles: - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } tasks: + - block: + - name: Gather Facts + setup: - - name: Gather Facts - setup: + - name: Checking the signature algorithm + local_action: > + shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 + become: no + register: sig_algo + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" - - name: Checking the signature algorithm - local_action: > - shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 - become: no - register: sig_algo - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + - name: Change the algorithm to RSA + set_fact: + algo_params: "rsa:2048" + when: '"ecdsa" not in sig_algo.stdout' - - name: Change the algorithm to RSA - set_fact: - algo_params: "rsa:2048" - when: '"ecdsa" not in sig_algo.stdout' + - name: Build the client's pair + local_action: > + shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && + openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + touch certs/{{ item }}_crt_generated + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ item }}_crt_generated + environment: + subjectAltName: "DNS:{{ item }}" + with_items: "{{ users }}" - - name: Build the client's pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && - openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && - touch certs/{{ item }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ item }}_crt_generated - environment: - subjectAltName: "DNS:{{ item }}" - with_items: "{{ users }}" + - name: Build the client's p12 + local_action: > + shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" - - name: Build the client's p12 - local_action: > - shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" + - name: Copy the p12 certificates + local_action: + module: copy + src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" + mode: 0600 + become: no + with_items: + - "{{ users }}" - - name: Copy the p12 certificates - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 - become: no - with_items: - - "{{ users }}" + - name: Get active users + local_action: > + shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + register: valid_certs - - name: Get active users - local_action: > - shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - register: valid_certs + - name: Revoke non-existing users + local_action: > + shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && + openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt + touch crl/{{ item }}_revoked + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: crl/{{ item }}_revoked + environment: + subjectAltName: "DNS:{{ item }}" + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" - - name: Revoke non-existing users - local_action: > - shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && - openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt - touch crl/{{ item }}_revoked - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: crl/{{ item }}_revoked - environment: - subjectAltName: "DNS:{{ item }}" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" + - name: Copy the revoked certificates to the vpn server + copy: + src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" + notify: + - rereadcrls - - name: Copy the revoked certificates to the vpn server - copy: - src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" - notify: - - rereadcrls + - name: Register p12 PayloadContent + local_action: > + shell cat private/{{ item }}.p12 | base64 + register: PayloadContent + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" - - name: Register p12 PayloadContent - local_action: > - shell cat private/{{ item }}.p12 | base64 - register: PayloadContent - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" + - name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - - name: Set facts for mobileconfigs - set_fact: - proxy_enabled: false - PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + - name: Build the mobileconfigs + local_action: + module: template + src: roles/vpn/templates/mobileconfig.j2 + dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + mode: 0600 + become: no + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + no_log: True - - name: Build the mobileconfigs - local_action: - module: template - src: roles/vpn/templates/mobileconfig.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig - mode: 0600 - become: no - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True + - name: Build the client ipsec config file + local_action: + module: template + src: roles/vpn/templates/client_ipsec.conf.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + mode: 0600 + become: no + with_items: + - "{{ users }}" - - name: Build the client ipsec config file - local_action: - module: template - src: roles/vpn/templates/client_ipsec.conf.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf - mode: 0600 - become: no - with_items: - - "{{ users }}" + - name: Build the client ipsec secret file + local_action: + module: template + src: roles/vpn/templates/client_ipsec.secrets.j2 + dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + mode: 0600 + become: no + with_items: + - "{{ users }}" - - name: Build the client ipsec secret file - local_action: - module: template - src: roles/vpn/templates/client_ipsec.secrets.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets - mode: 0600 - become: no - with_items: - - "{{ users }}" + - name: Build the windows client powershell script + local_action: + module: template + src: roles/vpn/templates/client_windows.ps1.j2 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + mode: 0600 + become: no + when: Win10_Enabled is defined and Win10_Enabled == "Y" + with_items: "{{ users }}" - - name: Build the windows client powershell script - local_action: - module: template - src: roles/vpn/templates/client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 - mode: 0600 - become: no - when: Win10_Enabled is defined and Win10_Enabled == "Y" - with_items: "{{ users }}" + # SSH - # SSH + - name: SSH | Get active system users + shell: > + getent group algo | cut -f4 -d: | sed "s/,/\n/g" + register: valid_users + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - - name: SSH | Get active system users - shell: > - getent group algo | cut -f4 -d: | sed "s/,/\n/g" - register: valid_users - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - - - name: SSH | Delete non-existing users - user: - name: "{{ item }}" - state: absent - remove: yes - force: yes - when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ valid_users.stdout_lines | default('null') }}" + - name: SSH | Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + with_items: "{{ valid_users.stdout_lines | default('null') }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always post_tasks: - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - tags: always + - block: + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" + tags: always + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always handlers: - name: rereadcrls From a97b210ee8a8269ab27189908953b2bf93971e78 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 29 Apr 2017 14:39:55 -0400 Subject: [PATCH 495/769] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eefd0447..a933401c 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua python-setuptools \ python-virtualenv -y ``` - - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/setup-redhat-centos6.md) - - Windows: See the [Windows documentation](docs/client-windows.md) + - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/deploy-from-redhat-centos6.md) + - Windows: See the [Windows documentation](docs/deploy-from-windows.md) 4. Install Algo's remaining dependencies for your operating system. Use the same terminal window as the previous step and run: ```bash From e3c5015f2eb5aa1a6dde98151f3d2835d7cf72ff Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 30 Apr 2017 14:28:44 -0400 Subject: [PATCH 496/769] Aws documentation (#505) * Add AWS and Cloudformation specific docs Closes #482 Closes #468 * readme enhancements * various grammatical issues fixed --- README.md | 38 ++++++++++++++++++-------------------- docs/troubleshooting.md | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a933401c..84972921 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). -2. [Download Algo](https://github.com/trailofbits/algo/archive/master.zip) and unzip it in a convenient location on your local machine. +2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. -3. Install Algo's core dependencies. Open the Terminal. The `python` interpreter you use to deploy Algo must be python2. If you don't know what this means, you're probably fine. `cd` into the `algo-master` directory where you unzipped Algo, then run: +3. **Install Algo's core dependencies.** Open the Terminal. The `python` interpreter you use to deploy Algo must be python2. If you don't know what this means, you're probably fine. `cd` into the `algo-master` directory where you unzipped Algo, then run: - macOS: ```bash @@ -58,15 +58,15 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua - Linux (rpm-based): See the [Pre-Install Documentation for RedHat/CentOS 6.x](docs/deploy-from-redhat-centos6.md) - Windows: See the [Windows documentation](docs/deploy-from-windows.md) -4. Install Algo's remaining dependencies for your operating system. Use the same terminal window as the previous step and run: +4. **Install Algo's remaining dependencies.** Use the same Terminal window as the previous step and run: ```bash $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. -5. Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. +5. **List the users to create.** Open `config.cfg` in your favorite text editor. Specify the users you wish to create in the `users` list. -6. Start the deployment. Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-from-ansible.md](docs/deploy-from-ansible.md). +6. **Start the deployment.** Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-from-ansible.md](docs/deploy-from-ansible.md). That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. @@ -84,23 +84,21 @@ You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to "\"#----------------------------------------------------------------------#\"", ``` -Advanced users who want to install Algo on top of a server they already own or want to script the deployment of Algo onto a network of servers, please see the [Deploy to Ubuntu](/docs/deploy-to-ubuntu.md) documentation. - ## Configure the VPN Clients -Distribute the configuration files to your users, so they can connect to the VPN. Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are saved under a subdirectory named with the IP address of your new Algo VPN server. +Certificates and configuration files that users will need are placed in the `configs` directory. Make sure to secure these files since many contain private keys. All files are saved under a subdirectory named with the IP address of your new Algo VPN server. ### Apple Devices -Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. +**Send users their Apple Profile.** Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. -On iOS, you can connect to the VPN by opening Settings and clicking the toggle next to "VPN" near the top of the list. On macOS, you can connect to the VPN by opening System Preferences -> Network, finding Algo VPN in the left column and clicking "Connect." On macOS, we recommend checking "Show VPN status in menu bar" too which lets you connect and disconnect from the menu bar. +**Turn on the VPN.** On iOS, connect to the VPN by opening Settings and clicking the toggle next to "VPN" near the top of the list. On macOS, connect to the VPN by opening System Preferences -> Network, finding Algo VPN in the left column and clicking "Connect." On macOS, check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. -If you enabled "On Demand", the VPN will connect automatically whenever it is able. On iOS, you can turn off "On Demand" by clicking the (i) next to the entry for Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "On Demand" by opening the Network Preferences, finding Algo VPN in the left column, and unchecking the box for "Connect on demand." +**Managing On-Demand VPNs.** If you enabled "On Demand", the VPN will connect automatically whenever it is able. On iOS, you can turn off "On Demand" by clicking the (i) next to the entry for Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "On Demand" by opening the Network Preferences, finding Algo VPN in the left column, and unchecking the box for "Connect on demand." ### Android Devices -You need to install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android) because no version of Android supports IKEv2. Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more detailed steps. +No version of Android supports IKEv2. Install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more a more detailed walkthrough. ### Windows @@ -132,7 +130,7 @@ Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransf ### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) -Network Manager does not support AES-GCM. In order to support Linux Desktop clients, please choose the "compatible" cryptography and use at least Network Manager 1.4.1. See [Issue #263](https://github.com/trailofbits/algo/issues/263) for more information. +Network Manager does not support AES-GCM. In order to support Linux Desktop clients, choose the "compatible" cryptography during the deploy process and use at least Network Manager 1.4.1. See [Issue #263](https://github.com/trailofbits/algo/issues/263) for more information. ### Linux strongSwan Clients (e.g., OpenWRT, Ubuntu Server, etc.) @@ -150,11 +148,11 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, 8. `sudo ipsec up `: start the ipsec tunnel 9. `sudo ipsec down `: shutdown the ipsec tunnel -One common use case is to let your server access your local LAN without going through the VPN. Set up a passthrough connection by adding the following to `/etc/ipsec.conf`. Replace `192.168.1.1/24` with the subnet your LAN uses: +One common use case is to let your server access your local LAN without going through the VPN. Set up a passthrough connection by adding the following to `/etc/ipsec.conf`: conn lan-passthrough - leftsubnet=192.168.1.1/24 - rightsubnet=192.168.1.1/24 + leftsubnet=192.168.1.1/24 # Replace with your LAN subnet + rightsubnet=192.168.1.1/24 # Replac with your LAND subnet authby=never # No authentication necessary type=pass # passthrough auto=route # no need to ipsec up lan-passthrough @@ -173,7 +171,7 @@ Depending on the platform, you may need one or multiple of the following files. ## Setup an SSH Tunnel -If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and an SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and only have limited tunneling options (e.g., `ssh -N` is required). This is done to ensure that SSH users have the least access required to tunnel through the server and can perform no other actions. +If you turned on the optional SSH tunneling role, then local user accounts will be created for each user in `config.cfg` and SSH authorized_key files for them will be in the `configs` directory (user.ssh.pem). SSH user accounts do not have shell access, cannot authenticate with a password, and only have limited tunneling options (e.g., `ssh -N` is required). This ensures that SSH users have the least access required to setup a tunnel and can perform no other actions on the Algo server. Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. @@ -185,20 +183,20 @@ To SSH into the Algo server for administrative purposes you can use the example `ssh ubuntu@ip -i ~/.ssh/algo.pem` -If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. +If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. `ssh-add ~/.ssh/algo > /dev/null 2>&1` ## Adding or Removing Users -Algo's own scripts can easily add and remove users from the VPN server. +If you chose the save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. 1. Update the `users` list in your `config.cfg` 2. Open a terminal, `cd` to the algo directory, and activate the virtual environment with `source env/bin/activate` 3. Run the command: `./algo update-users` -The Algo VPN server now contains only the users listed in the `config.cfg` file. +After this process completes, the Algo VPN server will contains only the users listed in the `config.cfg` file. ## Additional Documentation diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b22efae4..c66f0148 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -8,6 +8,8 @@ * [Error: "ansible-playbook: command not found"](#error-ansible-playbook-command-not-found) * [Bad owner or permissions on .ssh](#bad-owner-or-permissions-on-ssh) * [The region you want is not available](#the-region-you-want-is-not-available) + * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) + * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -126,6 +128,39 @@ You need to reset the permissions on your `.ssh` directory. Run `chmod 700 /home You want to install Algo to a specific region in a cloud provider, but that region is not available in the list given by the installer. In that case, you should [file an issue](https://github.com/trailofbits/algo/issues/new). Cloud providers add new regions on a regular basis and we don't always keep up. File an issue and give us information about what region is missing and we'll add it. +### AWS: SSH permission denied with an ECDSA key + +You tried to deploy Algo to AWS and you received an error like this one: + +``` +TASK [Copy the algo ssh key to the local ssh directory] ************************ +ok: [localhost -> localhost] + +PLAY [Configure the server and install required software] ********************** + +TASK [Check the system] ******************************************************** +fatal: [X.X.X.X]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Warning: Permanently added 'X.X.X.X' (ECDSA) to the list of known hosts.\r\nPermission denied (publickey).\r\n", "unreachable": true} +``` + +You previously deployed Algo to a hosting provider other than AWS, and Algo created an ECDSA keypair at that time. You are now deploying to AWS which [does not support ECDSA keys](https://aws.amazon.com/certificate-manager/faqs/) via their API. As a result, the deploy has failed. + +In order to fix this issue, delete the `algo.pem` and `algo.pem.pub` keys from your `configs` directory and run the deploy again. If AWS is selected, Algo will now generate new RSA ssh keys which are compatible with the AWS API. + +### AWS: "Deploy the template fails" with CREATE_FAILED + +You tried to deploy to Algo to AWS and you received an error like this one: + +``` +TASK [cloud-ec2 : Make a cloudformation template] ****************************** +changed: [localhost] + +TASK [cloud-ec2 : Deploy the template] ***************************************** +fatal: [localhost]: FAILED! => {"changed": true, "events": ["StackEvent AWS::CloudFormation::Stack algopvpn1 ROLLBACK_COMPLETE", "StackEvent AWS::EC2::VPC VPC DELETE_COMPLETE", "StackEvent AWS::EC2::InternetGateway InternetGateway DELETE_COMPLETE", "StackEvent AWS::CloudFormation::Stack algopvpn1 ROLLBACK_IN_PROGRESS", "StackEvent AWS::EC2::VPC VPC CREATE_FAILED", "StackEvent AWS::EC2::VPC VPC CREATE_IN_PROGRESS", "StackEvent AWS::EC2::InternetGateway InternetGateway CREATE_FAILED", "StackEvent AWS::EC2::InternetGateway InternetGateway CREATE_IN_PROGRESS", "StackEvent AWS::CloudFormation::Stack algopvpn1 CREATE_IN_PROGRESS"], "failed": true, "output": "Problem with CREATE. Rollback complete", "stack_outputs": {}, "stack_resources": [{"last_updated_time": null, "logical_resource_id": "InternetGateway", "physical_resource_id": null, "resource_type": "AWS::EC2::InternetGateway", "status": "DELETE_COMPLETE", "status_reason": null}, {"last_updated_time": null, "logical_resource_id": "VPC", "physical_resource_id": null, "resource_type": "AWS::EC2::VPC", "status": "DELETE_COMPLETE", "status_reason": null}]} +``` + +Algo builds a Cloudformation template to deploy to AWS. You can find the entire contents of the Cloudformation template in `configs/algo.yml`. In order to troubleshoot this issue, login to the AWS console, go to the Cloudformation service, find the failed deployment, click the events tab, and find the corresponding "CREATE_FAILED" events. Note that all AWS resources created by Algo are tagged with `Environment => Algo` for easy identification. + +In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. ## Connection Problems From 6527d04a6f5364fba6620bc1bc50f096cef3eef8 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 30 Apr 2017 15:44:47 -0400 Subject: [PATCH 497/769] add FAQ about software updates (#506) * add FAQ about software updates * toc * grammar * grammar * link * grammar --- docs/faq.md | 11 ++++++++++- docs/troubleshooting.md | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index dc49f106..c7bac984 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -6,6 +6,7 @@ * [Why aren't you using a memory-safe or verified IKE daemon?](#why-arent-you-using-a-memory-safe-or-verified-ike-daemon) * [Why aren't you using OpenVPN?](#why-arent-you-using-openvpn) * [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) +* [I deployed an Algo server. Can you update it with new features?](#i-deployed-an-algo-server-can-you-update-it-with-new-features) * [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) ## Has Algo been audited? @@ -14,7 +15,7 @@ No. This project is under active development. We're happy to [accept and fix iss ## Why aren't you using Tor? -The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic while traveling. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). +The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). ## Why aren't you using Racoon, LibreSwan, or OpenSwan? @@ -32,6 +33,14 @@ OpenVPN does not have out-of-the-box client support on any major desktop or mobi Alpine Linux is not supported out-of-the-box by any major cloud provider. We are interested in supporting Free-, Open-, and HardenedBSD. Follow along or contribute to our BSD support in [this issue](https://github.com/trailofbits/algo/issues/35). +## I deployed an Algo server. Can you update it with new features? + +No. By design, the Algo development team has no access to any Algo server that our users haved deployed. We cannot modify the configuration, update the software, or sniff the traffic that goes through your personal Algo VPN server. This prevents scenarios where we are legally compelled or hacked to push down backdoored updates that surveil our users. + +As a result, once your Algo server has been deployed, it is yours to maintain. If you want to take advantage of new features available in the current release of Algo, then you have two options. You can use the [SSH administrative interface](/README.md#ssh-into-algo-server) to make the changes you want on your own or you can shut down the server and deploy a new one (recommended). + +In the future, we will make it easier for users who want to update their own servers by providing official releases of Algo. Each official release will summarize the changes from the last release to make it easier to follow along with them. + ## Where did the name "Algo" come from? Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c66f0148..9a2bc0c8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -158,7 +158,7 @@ TASK [cloud-ec2 : Deploy the template] ***************************************** fatal: [localhost]: FAILED! => {"changed": true, "events": ["StackEvent AWS::CloudFormation::Stack algopvpn1 ROLLBACK_COMPLETE", "StackEvent AWS::EC2::VPC VPC DELETE_COMPLETE", "StackEvent AWS::EC2::InternetGateway InternetGateway DELETE_COMPLETE", "StackEvent AWS::CloudFormation::Stack algopvpn1 ROLLBACK_IN_PROGRESS", "StackEvent AWS::EC2::VPC VPC CREATE_FAILED", "StackEvent AWS::EC2::VPC VPC CREATE_IN_PROGRESS", "StackEvent AWS::EC2::InternetGateway InternetGateway CREATE_FAILED", "StackEvent AWS::EC2::InternetGateway InternetGateway CREATE_IN_PROGRESS", "StackEvent AWS::CloudFormation::Stack algopvpn1 CREATE_IN_PROGRESS"], "failed": true, "output": "Problem with CREATE. Rollback complete", "stack_outputs": {}, "stack_resources": [{"last_updated_time": null, "logical_resource_id": "InternetGateway", "physical_resource_id": null, "resource_type": "AWS::EC2::InternetGateway", "status": "DELETE_COMPLETE", "status_reason": null}, {"last_updated_time": null, "logical_resource_id": "VPC", "physical_resource_id": null, "resource_type": "AWS::EC2::VPC", "status": "DELETE_COMPLETE", "status_reason": null}]} ``` -Algo builds a Cloudformation template to deploy to AWS. You can find the entire contents of the Cloudformation template in `configs/algo.yml`. In order to troubleshoot this issue, login to the AWS console, go to the Cloudformation service, find the failed deployment, click the events tab, and find the corresponding "CREATE_FAILED" events. Note that all AWS resources created by Algo are tagged with `Environment => Algo` for easy identification. +Algo builds a [Cloudformation](https://aws.amazon.com/cloudformation/) template to deploy to AWS. You can find the entire contents of the Cloudformation template in `configs/algo.yml`. In order to troubleshoot this issue, login to the AWS console, go to the Cloudformation service, find the failed deployment, click the events tab, and find the corresponding "CREATE_FAILED" events. Note that all AWS resources created by Algo are tagged with `Environment => Algo` for easy identification. In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. From 9f698fdd685e53d417f035e571f2b397aa3532dc Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 3 May 2017 22:03:10 +0200 Subject: [PATCH 498/769] Get strongswan from the Zesty repo on Xenial (#515) --- roles/vpn/tasks/ubuntu-hacks.yml | 14 ++++++++++++++ roles/vpn/tasks/ubuntu.yml | 3 +++ roles/vpn/templates/01_strongswan.pref.j2 | 3 +++ roles/vpn/templates/01_xenial_aptconf.j2 | 1 + 4 files changed, 21 insertions(+) create mode 100644 roles/vpn/tasks/ubuntu-hacks.yml create mode 100644 roles/vpn/templates/01_strongswan.pref.j2 create mode 100644 roles/vpn/templates/01_xenial_aptconf.j2 diff --git a/roles/vpn/tasks/ubuntu-hacks.yml b/roles/vpn/tasks/ubuntu-hacks.yml new file mode 100644 index 00000000..fbe2cbc7 --- /dev/null +++ b/roles/vpn/tasks/ubuntu-hacks.yml @@ -0,0 +1,14 @@ +--- + +- name: Configure apt to use the Xenial release by default + template: src=01_xenial_aptconf.j2 dest=/etc/apt/apt.conf.d/01xenial + +- name: Configure packages preferences + template: src=01_strongswan.pref.j2 dest=/etc/apt/preferences.d/01_strongswan.pref + +- name: Configure the Ubuntu Zesty repository + apt_repository: + repo: deb http://mirrors.kernel.org/ubuntu/ zesty main + state: present + filename: 'zesty' + update_cache: yes diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index 4856a97c..971d905e 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -3,6 +3,9 @@ - set_fact: strongswan_additional_plugins: [] +- include: ubuntu-hacks.yml + when: ansible_distribution_version == "16.04" + - name: Ubuntu | Install strongSwan apt: name=strongswan state=latest update_cache=yes install_recommends=yes diff --git a/roles/vpn/templates/01_strongswan.pref.j2 b/roles/vpn/templates/01_strongswan.pref.j2 new file mode 100644 index 00000000..3249758f --- /dev/null +++ b/roles/vpn/templates/01_strongswan.pref.j2 @@ -0,0 +1,3 @@ +Package: *strongswan* +Pin: release n=zesty +Pin-Priority: 9000 diff --git a/roles/vpn/templates/01_xenial_aptconf.j2 b/roles/vpn/templates/01_xenial_aptconf.j2 new file mode 100644 index 00000000..c589ffcd --- /dev/null +++ b/roles/vpn/templates/01_xenial_aptconf.j2 @@ -0,0 +1 @@ +APT::Default-Release "xenial"; From 6f170982aa876526d2641b2bf3a69905f8ca6671 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 4 May 2017 14:33:31 +0200 Subject: [PATCH 499/769] move to Elastic IP (#512) --- roles/cloud-ec2/tasks/main.yml | 4 ++-- roles/cloud-ec2/templates/stack.yml.j2 | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 6c397a52..e32e70a5 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -26,7 +26,7 @@ - name: Add new instance to host group add_host: - hostname: "{{ stack.stack_outputs.PublicIP }}" + hostname: "{{ stack.stack_outputs.ElasticIP }}" groupname: vpn-host ansible_ssh_user: ubuntu ansible_python_interpreter: "/usr/bin/python2.7" @@ -35,7 +35,7 @@ ipv6_support: yes - set_fact: - cloud_instance_ip: "{{ stack.stack_outputs.PublicIP }}" + cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" - name: Get EC2 instances ec2_remote_facts: diff --git a/roles/cloud-ec2/templates/stack.yml.j2 b/roles/cloud-ec2/templates/stack.yml.j2 index 1678413b..8d9cca20 100644 --- a/roles/cloud-ec2/templates/stack.yml.j2 +++ b/roles/cloud-ec2/templates/stack.yml.j2 @@ -36,7 +36,7 @@ Resources: Type: AWS::EC2::Subnet Properties: CidrBlock: {{ ec2_vpc_nets.subnet_cidr }} - MapPublicIpOnLaunch: true + MapPublicIpOnLaunch: false Tags: - Key: Environment Value: Algo @@ -184,9 +184,13 @@ Resources: - Key: Environment Value: Algo -Outputs: - PublicIP: - Value: - Fn::GetAtt: + ElasticIP: + Type: AWS::EC2::EIP + Properties: + InstanceId: !Ref EC2Instance + DependsOn: - EC2Instance - - PublicIp + +Outputs: + ElasticIP: + Value: !Ref ElasticIP From a225bde2b8454380017e09ac23ac8d876594311f Mon Sep 17 00:00:00 2001 From: "Christopher J. Pilkington" Date: Sat, 6 May 2017 09:16:28 -0400 Subject: [PATCH 500/769] Specify EIP domain (#521) --- roles/cloud-ec2/templates/stack.yml.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/cloud-ec2/templates/stack.yml.j2 b/roles/cloud-ec2/templates/stack.yml.j2 index 8d9cca20..694386f8 100644 --- a/roles/cloud-ec2/templates/stack.yml.j2 +++ b/roles/cloud-ec2/templates/stack.yml.j2 @@ -187,9 +187,11 @@ Resources: ElasticIP: Type: AWS::EC2::EIP Properties: + Domain: vpc InstanceId: !Ref EC2Instance DependsOn: - EC2Instance + - VPCGatewayAttachment Outputs: ElasticIP: From 25b6ab9e0a35c2affbbcf599b35218f609cce72e Mon Sep 17 00:00:00 2001 From: Osman Surkatty Date: Sat, 6 May 2017 06:16:35 -0700 Subject: [PATCH 501/769] Added missing minimum policy actions. (#522) Going through the installation process it appears that you're missing the following calls: `ec2:describeAddresses` and `ec2:allocateAddress`. This change fixes that. --- docs/deploy-from-ansible.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 54154bbb..46a3cdfa 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -157,7 +157,9 @@ Additional tags: "ec2:DescribeSecurityGroups", "ec2:AuthorizeSecurityGroupIngress", "ec2:RunInstances", - "ec2:DescribeInstances" + "ec2:DescribeInstances", + "ec2:AllocateAddress", + "ec2:DescribeAddresses" ], "Resource": [ "*" From 27f9cda361e4e536443a046927727352b4c93465 Mon Sep 17 00:00:00 2001 From: "Christopher J. Pilkington" Date: Sun, 7 May 2017 12:35:27 -0400 Subject: [PATCH 502/769] Add additional delay for ec2 instance prior to ssh (#527) * Add additional delay for ec2 instance prior to ssh * Add 10 second delay to all, rather than to cloud-ec2 --- playbooks/post.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/post.yml b/playbooks/post.yml index b51d0ca4..3f1c1423 100644 --- a/playbooks/post.yml +++ b/playbooks/post.yml @@ -12,6 +12,6 @@ - name: A short pause, in order to be sure the instance is ready pause: - seconds: 10 + seconds: 20 - include: local_ssh.yml From 0031d2809ed19b2705f1a89f317bb2ca827bbd45 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 21:40:23 +0200 Subject: [PATCH 503/769] Disable the Signature Algorithm check and add default vars. Fixes #525 --- deploy_client.yml | 1 + roles/client/tasks/main.yml | 13 ------------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/deploy_client.yml b/deploy_client.yml index baf26c81..4a069559 100644 --- a/deploy_client.yml +++ b/deploy_client.yml @@ -18,6 +18,7 @@ become: true vars_files: - config.cfg + - roles/vpn/defaults/main.yml pre_tasks: - name: Get the OS diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 9a2fe0aa..c75d2faf 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -4,19 +4,6 @@ - name: Include system based facts and tasks include: systems/main.yml -- name: Checking the signature algorithm - local_action: > - shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 - become: no - register: sig_algo - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - -- name: Change the algorithm to RSA - set_fact: - Win10_Enabled: "Y" - when: '"ecdsa" not in sig_algo.stdout' - - name: Install prerequisites package: name="{{ item }}" state=present with_items: From 627b7d5d9b538adb7007cfdac43e2d988de5ed6e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 22:10:59 +0200 Subject: [PATCH 504/769] define local_dns if dns tag used (#531) --- algo | 2 +- playbooks/facts/main.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 901bc0f4..b8f1ac91 100755 --- a/algo +++ b/algo @@ -40,7 +40,7 @@ read -p " Do you want to install a DNS resolver on this VPN server, to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi read -p " Do you want each user to have their own account for SSH tunneling? diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index 7c8516d7..8c25d6d6 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -35,3 +35,9 @@ - name: Define the commonName set_fact: IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: The DNS tag is defined + set_fact: + local_dns: Y + tags: + - dns From d10a86b33175ce2befa70a38730b86c3560a6301 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 22:12:49 +0200 Subject: [PATCH 505/769] Revert "define local_dns if dns tag used (#531)" (#532) This reverts commit 627b7d5d9b538adb7007cfdac43e2d988de5ed6e. --- algo | 2 +- playbooks/facts/main.yml | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/algo b/algo index b8f1ac91..901bc0f4 100755 --- a/algo +++ b/algo @@ -40,7 +40,7 @@ read -p " Do you want to install a DNS resolver on this VPN server, to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi read -p " Do you want each user to have their own account for SSH tunneling? diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index 8c25d6d6..7c8516d7 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -35,9 +35,3 @@ - name: Define the commonName set_fact: IP_subject_alt_name: "{{ IP_subject_alt_name }}" - -- name: The DNS tag is defined - set_fact: - local_dns: Y - tags: - - dns From 97369c303ae93b9a1802430f6babad9955847f44 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 22:33:30 +0200 Subject: [PATCH 506/769] define local_dns if dns tag used (#533) --- algo | 2 +- roles/dns_adblocking/tasks/main.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 901bc0f4..b8f1ac91 100755 --- a/algo +++ b/algo @@ -40,7 +40,7 @@ read -p " Do you want to install a DNS resolver on this VPN server, to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=Y"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi read -p " Do you want each user to have their own account for SSH tunneling? diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index f336d23e..996443fa 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,5 +1,10 @@ --- - block: + + - name: The DNS tag is defined + set_fact: + local_dns: Y + - name: Dnsmasq installed package: name=dnsmasq From 07ddb5863b265ce15ebf74ce7638c2ef981a9181 Mon Sep 17 00:00:00 2001 From: Ruben Jongejan Date: Mon, 8 May 2017 22:34:24 +0200 Subject: [PATCH 507/769] improved readability with native yaml (#530) --- roles/dns_adblocking/tasks/main.yml | 16 ++++- roles/dns_adblocking/tasks/ubuntu.yml | 18 ++++- roles/security/tasks/main.yml | 95 ++++++++++++++++++++++---- roles/ssh_tunneling/tasks/main.yml | 15 ++-- roles/vpn/tasks/freebsd.yml | 11 +-- roles/vpn/tasks/ipec_configuration.yml | 6 +- roles/vpn/tasks/iptables.yml | 14 +++- roles/vpn/tasks/main.yml | 3 +- roles/vpn/tasks/ubuntu-hacks.yml | 8 ++- roles/vpn/tasks/ubuntu.yml | 17 ++++- 10 files changed, 164 insertions(+), 39 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 996443fa..7e85e0ea 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -21,12 +21,19 @@ when: ansible_distribution == 'FreeBSD' - name: Dnsmasq configured - template: src=dnsmasq.conf.j2 dest="{{ config_prefix|default('/') }}etc/dnsmasq.conf" + template: + src: dnsmasq.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/dnsmasq.conf" notify: - restart dnsmasq - name: Adblock script created - template: src=adblock.sh dest=/usr/local/sbin/adblock.sh owner=root group="{{ root_group|default('root') }}" mode=0755 + template: + src: adblock.sh + dest: /usr/local/sbin/adblock.sh + owner: root + group: "{{ root_group|default('root') }}" + mode: 0755 - name: Adblock script added to cron cron: @@ -43,7 +50,10 @@ - meta: flush_handlers - name: Dnsmasq enabled and started - service: name=dnsmasq state=started enabled=yes + service: + name: dnsmasq + state: started + enabled: yes rescue: - debug: var=fail_hint tags: always diff --git a/roles/dns_adblocking/tasks/ubuntu.yml b/roles/dns_adblocking/tasks/ubuntu.yml index f0ffb915..8e4cf3d0 100644 --- a/roles/dns_adblocking/tasks/ubuntu.yml +++ b/roles/dns_adblocking/tasks/ubuntu.yml @@ -1,7 +1,12 @@ --- - name: Ubuntu | Dnsmasq profile for apparmor configured - template: src=usr.sbin.dnsmasq.j2 dest=/etc/apparmor.d/usr.sbin.dnsmasq owner=root group=root mode=0600 + template: + src: usr.sbin.dnsmasq.j2 + dest: /etc/apparmor.d/usr.sbin.dnsmasq + owner: root + group: root + mode: 0600 when: apparmor_enabled is defined and apparmor_enabled == true notify: - restart dnsmasq @@ -12,10 +17,17 @@ tags: ['apparmor'] - name: Ubuntu | Ensure that the dnsmasq service directory exist - file: path=/etc/systemd/system/dnsmasq.service.d/ state=directory mode=0755 owner=root group=root + file: + path: /etc/systemd/system/dnsmasq.service.d/ + state: directory + mode: 0755 + owner: root + group: root - name: Ubuntu | Setup the cgroup limitations for the ipsec daemon - template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf + template: + src: 100-CustomLimitations.conf.j2 + dest: /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf notify: - daemon-reload - restart dnsmasq diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 657e0c19..4289ad1f 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -6,10 +6,20 @@ - unattended-upgrades - name: Configure unattended-upgrades - template: src=50unattended-upgrades.j2 dest=/etc/apt/apt.conf.d/50unattended-upgrades owner=root group=root mode=0644 + template: + src: 50unattended-upgrades.j2 + dest: /etc/apt/apt.conf.d/50unattended-upgrades + owner: root + group: root + mode: 0644 - name: Periodic upgrades configured - template: src=10periodic.j2 dest=/etc/apt/apt.conf.d/10periodic owner=root group=root mode=0644 + template: + src: 10periodic.j2 + dest: /etc/apt/apt.conf.d/10periodic + owner: root + group: root + mode: 0644 - name: Find directories for minimizing access stat: @@ -24,16 +34,27 @@ - '/bin' - name: Minimize access - file: path='{{ item.stat.path }}' mode='go-w' recurse=yes + file: + path: '{{ item.stat.path }}' + mode: 'go-w' + recurse: yes when: item.stat.isdir with_items: "{{ minimize_access_directories.results }}" no_log: True - name: Change shadow ownership to root and mode to 0600 - file: dest='/etc/shadow' owner=root group=root mode=0600 + file: + dest: '/etc/shadow' + owner: root + group: root + mode: 0600 - name: change su-binary to only be accessible to user and group root - file: dest='/bin/su' owner=root group=root mode=0750 + file: + dest: '/bin/su' + owner: root + group: root + mode: 0750 - name: Collect Use of privileged commands shell: > @@ -45,15 +66,30 @@ # Core dumps - name: Restrict core dumps (with PAM) - lineinfile: dest=/etc/security/limits.conf line="* hard core 0" state=present + lineinfile: + dest: /etc/security/limits.conf + line: "* hard core 0" + state: present - name: Restrict core dumps (with sysctl) - sysctl: name=fs.suid_dumpable value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: fs.suid_dumpable + value: 0 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present # Kernel fixes - name: Disable Source Routed Packet Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: "{{item}}" + value: 0 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present with_items: - net.ipv4.conf.all.accept_source_route - net.ipv4.conf.default.accept_source_route @@ -61,13 +97,25 @@ - flush routing cache - name: Disable ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: "{{item}}" + value: 0 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present with_items: - net.ipv4.conf.all.accept_redirects - net.ipv4.conf.default.accept_redirects - name: Disable Secure ICMP Redirect Acceptance - sysctl: name="{{item}}" value=0 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: "{{item}}" + value: 0 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present with_items: - net.ipv4.conf.all.secure_redirects - net.ipv4.conf.default.secure_redirects @@ -75,12 +123,24 @@ - flush routing cache - name: Enable Bad Error Message Protection - sysctl: name=net.ipv4.icmp_ignore_bogus_error_responses value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: net.ipv4.icmp_ignore_bogus_error_responses + value: 1 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present notify: - flush routing cache - name: Enable RFC-recommended Source Route Validation - sysctl: name="{{item}}" value=1 ignoreerrors=yes sysctl_set=yes reload=yes state=present + sysctl: + name: "{{item}}" + value: 1 + ignoreerrors: yes + sysctl_set: yes + reload: yes + state: present with_items: - net.ipv4.conf.all.rp_filter - net.ipv4.conf.default.rp_filter @@ -88,10 +148,17 @@ - flush routing cache - name: Do not send ICMP redirects (we are not a router) - sysctl: name=net.ipv4.conf.all.send_redirects value=0 + sysctl: + name: net.ipv4.conf.all.send_redirects + value: 0 - name: SSH config - template: src=sshd_config.j2 dest=/etc/ssh/sshd_config owner=root group=root mode=0644 + template: + src: sshd_config.j2 + dest: /etc/ssh/sshd_config + owner: root + group: root + mode: 0644 notify: - restart ssh rescue: diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 8d1b3a3e..90ff26f8 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -18,7 +18,12 @@ group: name=algo state=present - name: Ensure that the jail directory exist - file: path=/var/jail/ state=directory mode=0755 owner=root group="{{ root_group|default('root') }}" + file: + path: /var/jail/ + state: directory + mode: 0755 + owner: root + group: "{{ root_group|default('root') }}" - name: Ensure that the SSH users exist user: @@ -46,12 +51,14 @@ with_items: "{{ users }}" - name: Generate SSH fingerprints - shell: > - ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null + shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null register: ssh_fingerprints - name: Fetch users SSH private keys - fetch: src='/var/jail/{{ item }}/.ssh/id_ecdsa' dest=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat=yes + fetch: + src: '/var/jail/{{ item }}/.ssh/id_ecdsa' + dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem + flat: yes with_items: "{{ users }}" - name: Change mode for SSH private keys diff --git a/roles/vpn/tasks/freebsd.yml b/roles/vpn/tasks/freebsd.yml index 8964faa1..1dbecd5f 100644 --- a/roles/vpn/tasks/freebsd.yml +++ b/roles/vpn/tasks/freebsd.yml @@ -15,8 +15,7 @@ - "crypto" - name: FreeBSD / HardenedBSD | Make the kernel config - shell: > - sysctl -b kern.conftxt > /tmp/IPSEC + shell: sysctl -b kern.conftxt > /tmp/IPSEC when: rebuild_needed is defined and rebuild_needed == true - name: FreeBSD / HardenedBSD | Ensure the all options are enabled @@ -100,8 +99,7 @@ msg: "Something went wrong. Check the debug output above." - name: FreeBSD / HardenedBSD | Reboot - shell: > - sleep 2 && shutdown -r now + shell: sleep 2 && shutdown -r now args: executable: /usr/local/bin/bash when: rebuild_needed is defined and rebuild_needed == true @@ -110,4 +108,7 @@ ignore_errors: true - name: FreeBSD / HardenedBSD | Enable strongswan - lineinfile: dest=/etc/rc.conf regexp=^strongswan_enable= line='strongswan_enable="YES"' + lineinfile: + dest: /etc/rc.conf + regexp: ^strongswan_enable= + line: 'strongswan_enable="YES"' diff --git a/roles/vpn/tasks/ipec_configuration.yml b/roles/vpn/tasks/ipec_configuration.yml index a6b1530b..cc7c21ec 100644 --- a/roles/vpn/tasks/ipec_configuration.yml +++ b/roles/vpn/tasks/ipec_configuration.yml @@ -32,7 +32,11 @@ register: strongswan_plugins - name: Disable unneeded plugins - lineinfile: dest="{{ config_prefix|default('/') }}etc/strongswan.d/charon/{{ item }}.conf" regexp='.*load.*' line='load = no' state=present + lineinfile: + dest: "{{ config_prefix|default('/') }}etc/strongswan.d/charon/{{ item }}.conf" + regexp: '.*load.*' + line: 'load = no' + state: present notify: - restart strongswan when: item not in strongswan_enabled_plugins and item not in strongswan_additional_plugins diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml index fc065c37..251335d6 100644 --- a/roles/vpn/tasks/iptables.yml +++ b/roles/vpn/tasks/iptables.yml @@ -1,14 +1,24 @@ --- - name: Iptables configured - template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: root + group: root + mode: 0640 with_items: - { src: rules.v4.j2, dest: /etc/iptables/rules.v4 } notify: - restart iptables - name: Iptables configured - template: src="{{ item.src }}" dest="{{ item.dest }}" owner=root group=root mode=0640 + template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + owner: root + group: root + mode: 0640 when: ipv6_support is defined and ipv6_support == true with_items: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 5e26ac59..d250caff 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -16,8 +16,7 @@ package: name=strongswan state=present - name: Get StrongSwan versions - shell: > - ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" + shell: ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" register: strongswan_version - include: ipec_configuration.yml diff --git a/roles/vpn/tasks/ubuntu-hacks.yml b/roles/vpn/tasks/ubuntu-hacks.yml index fbe2cbc7..a64b754c 100644 --- a/roles/vpn/tasks/ubuntu-hacks.yml +++ b/roles/vpn/tasks/ubuntu-hacks.yml @@ -1,10 +1,14 @@ --- - name: Configure apt to use the Xenial release by default - template: src=01_xenial_aptconf.j2 dest=/etc/apt/apt.conf.d/01xenial + template: + src: 01_xenial_aptconf.j2 + dest: /etc/apt/apt.conf.d/01xenial - name: Configure packages preferences - template: src=01_strongswan.pref.j2 dest=/etc/apt/preferences.d/01_strongswan.pref + template: + src: 01_strongswan.pref.j2 + dest: /etc/apt/preferences.d/01_strongswan.pref - name: Configure the Ubuntu Zesty repository apt_repository: diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index 971d905e..db046ad4 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -7,7 +7,11 @@ when: ansible_distribution_version == "16.04" - name: Ubuntu | Install strongSwan - apt: name=strongswan state=latest update_cache=yes install_recommends=yes + apt: + name: strongswan + state: latest + update_cache: yes + install_recommends: yes - name: Ubuntu | Enforcing ipsec with apparmor shell: aa-enforce "{{ item }}" @@ -28,10 +32,17 @@ - netfilter-persistent - name: Ubuntu | Ensure that the strongswan service directory exist - file: path=/etc/systemd/system/strongswan.service.d/ state=directory mode=0755 owner=root group=root + file: + path: /etc/systemd/system/strongswan.service.d/ + state: directory + mode: 0755 + owner: root + group: root - name: Ubuntu | Setup the cgroup limitations for the ipsec daemon - template: src=100-CustomLimitations.conf.j2 dest=/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf + template: + src: 100-CustomLimitations.conf.j2 + dest: /etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf notify: - daemon-reload - restart strongswan From 58d5a06e87f861700478bb1999a32c1c5af2ada3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 22:34:45 +0200 Subject: [PATCH 508/769] delete tasks and move to roles (#519) --- algo | 2 +- roles/ssh_tunneling/tasks/main.yml | 15 +++ roles/vpn/handlers/main.yml | 3 + roles/vpn/tasks/main.yml | 2 + roles/vpn/tasks/openssl.yml | 31 ++++++ users.yml | 168 +---------------------------- 6 files changed, 56 insertions(+), 165 deletions(-) diff --git a/algo b/algo index b8f1ac91..551dbe4d 100755 --- a/algo +++ b/algo @@ -440,7 +440,7 @@ Enter the password for the private CA key: $ADDITIONAL_PROMPT : " -rs easyrsa_CA_password -ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" +ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" -t update-users --skip-tags common } case "$1" in diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 90ff26f8..35161bca 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -82,6 +82,21 @@ become: no with_items: - "{{ users }}" + + - name: SSH | Get active system users + shell: > + getent group algo | cut -f4 -d: | sed "s/,/\n/g" + register: valid_users + when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + + - name: SSH | Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + with_items: "{{ valid_users.stdout_lines | default('null') }}" rescue: - debug: var=fail_hint tags: always diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 32885b5f..9b481d43 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -12,3 +12,6 @@ - name: restart iptables service: name=netfilter-persistent state=restarted + +- name: rereadcrls + shell: ipsec rereadcrls diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index d250caff..9a9c9277 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -21,8 +21,10 @@ - include: ipec_configuration.yml - include: openssl.yml + tags: update-users - include: distribute_keys.yml - include: client_configs.yml + tags: update-users - meta: flush_handlers diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 8f9d52ab..8c84a9b9 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -115,3 +115,34 @@ become: no with_items: - "{{ users }}" + +- name: Get active users + local_action: > + shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + register: valid_certs + +- name: Revoke non-existing users + local_action: > + shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && + openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt + touch crl/{{ item }}_revoked + become: no + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: crl/{{ item }}_revoked + environment: + subjectAltName: "DNS:{{ item }}" + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" + +- name: Copy the revoked certificates to the vpn server + copy: + src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" + notify: + - rereadcrls diff --git a/users.yml b/users.yml index 5f55bc0d..a9be55e6 100644 --- a/users.yml +++ b/users.yml @@ -2,6 +2,7 @@ - hosts: localhost gather_facts: False + tags: always vars_files: - config.cfg @@ -40,12 +41,12 @@ become: true vars_files: - config.cfg - - roles/vpn/defaults/main.yml pre_tasks: - block: - name: Common pre-tasks include: playbooks/common.yml + tags: always rescue: - debug: var=fail_hint tags: always @@ -53,165 +54,8 @@ tags: always roles: - - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ], when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - tasks: - - block: - - name: Gather Facts - setup: - - - name: Checking the signature algorithm - local_action: > - shell openssl x509 -text -in certs/{{ IP_subject_alt_name }}.crt | grep 'Signature Algorithm' | head -n1 - become: no - register: sig_algo - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - - - name: Change the algorithm to RSA - set_fact: - algo_params: "rsa:2048" - when: '"ecdsa" not in sig_algo.stdout' - - - name: Build the client's pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && - openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && - touch certs/{{ item }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ item }}_crt_generated - environment: - subjectAltName: "DNS:{{ item }}" - with_items: "{{ users }}" - - - name: Build the client's p12 - local_action: > - shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - - - name: Copy the p12 certificates - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 - become: no - with_items: - - "{{ users }}" - - - name: Get active users - local_action: > - shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - register: valid_certs - - - name: Revoke non-existing users - local_action: > - shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && - openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt - touch crl/{{ item }}_revoked - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: crl/{{ item }}_revoked - environment: - subjectAltName: "DNS:{{ item }}" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" - - - name: Copy the revoked certificates to the vpn server - copy: - src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" - notify: - - rereadcrls - - - name: Register p12 PayloadContent - local_action: > - shell cat private/{{ item }}.p12 | base64 - register: PayloadContent - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - - - name: Set facts for mobileconfigs - set_fact: - proxy_enabled: false - PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - - - name: Build the mobileconfigs - local_action: - module: template - src: roles/vpn/templates/mobileconfig.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig - mode: 0600 - become: no - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - - - name: Build the client ipsec config file - local_action: - module: template - src: roles/vpn/templates/client_ipsec.conf.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf - mode: 0600 - become: no - with_items: - - "{{ users }}" - - - name: Build the client ipsec secret file - local_action: - module: template - src: roles/vpn/templates/client_ipsec.secrets.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets - mode: 0600 - become: no - with_items: - - "{{ users }}" - - - name: Build the windows client powershell script - local_action: - module: template - src: roles/vpn/templates/client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 - mode: 0600 - become: no - when: Win10_Enabled is defined and Win10_Enabled == "Y" - with_items: "{{ users }}" - - # SSH - - - name: SSH | Get active system users - shell: > - getent group algo | cut -f4 -d: | sed "s/,/\n/g" - register: valid_users - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - - - name: SSH | Delete non-existing users - user: - name: "{{ item }}" - state: absent - remove: yes - force: yes - when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" - with_items: "{{ valid_users.stdout_lines | default('null') }}" - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - { role: ssh_tunneling, tags: always, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - { role: vpn } post_tasks: - block: @@ -225,7 +69,3 @@ tags: always - fail: tags: always - - handlers: - - name: rereadcrls - shell: ipsec rereadcrls From 1b56dd660b5f73a2c8f22d1d8a39f586aa86e8c4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 8 May 2017 22:39:18 +0200 Subject: [PATCH 509/769] Update docs about sudo #529 --- docs/client-linux.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/client-linux.md b/docs/client-linux.md index aabd5f3e..d0772cc9 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -17,3 +17,6 @@ The playbook is `deploy_client.yml` ```shell ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root' ``` + +### Additional options: +If the user requires sudo password use the following argument: `--ask-become-pass` From fd5433efed13d3cd0ba59052cd93f9ea3af9446a Mon Sep 17 00:00:00 2001 From: Ruben Jongejan Date: Tue, 9 May 2017 21:43:34 +0200 Subject: [PATCH 510/769] renamed localhost group to fix duplicate naming of group&host (#537) --- inventory | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventory b/inventory index 2f0f3adf..e308ef74 100644 --- a/inventory +++ b/inventory @@ -1,2 +1,2 @@ -[localhost] +[local] localhost ansible_connection=local ansible_python_interpreter=python From ac6db06a191655b3dfdf042567907b23207de9dd Mon Sep 17 00:00:00 2001 From: tetov Date: Wed, 10 May 2017 16:06:19 +0200 Subject: [PATCH 511/769] grammar edit (#540) * grammar edit * Update openssl.yml --- roles/vpn/tasks/openssl.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 8c84a9b9..44fd9086 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -1,6 +1,6 @@ --- -- name: Ensure the pki directory is not exist +- name: Ensure the pki directory does not exist local_action: module: file dest: configs/{{ IP_subject_alt_name }}/pki @@ -8,7 +8,7 @@ become: no when: easyrsa_reinit_existent == True -- name: Ensure the pki directories are exist +- name: Ensure the pki directories exist local_action: module: file dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" @@ -23,7 +23,7 @@ - private - reqs -- name: Ensure the files are exist +- name: Ensure the files exist local_action: module: file dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" @@ -43,7 +43,6 @@ dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" become: no - - name: Build the CA pair local_action: > shell openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && From bc604fb3e238c33e44a0f588f0a67d20181c02ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Job=20Evers=E2=80=90Meltzer?= Date: Sat, 13 May 2017 11:25:36 -0500 Subject: [PATCH 512/769] Update instructions on README (#547) Tweaked README instructions as the paths were slightly different. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 84972921..4ef47570 100644 --- a/README.md +++ b/README.md @@ -139,11 +139,11 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, #### Ubuntu Server 16.04 example 1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan -2. `/etc/ipsec.d/certs`: copy `user.crt` from `algo-master/configs//pki/certs` -3. `/etc/ipsec.d/private`: copy `user.key` from `algo-master/configs//pki/private` -4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//cacert.pem` -5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. `xx.xxx.xx.xxx : ECDSA user.key` -6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and update `leftcert` to match the `user.crt` filename +2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` +3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//pki/private/.key` +4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//pki/cacert.pem` +5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. ` : ECDSA .key` +6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and ensure `leftcert` matches the `.crt` filename 7. `sudo ipsec restart`: pick up config changes 8. `sudo ipsec up `: start the ipsec tunnel 9. `sudo ipsec down `: shutdown the ipsec tunnel From 75d64ac018bfba062119fcd960641abfde80306e Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 15 May 2017 20:39:34 +1000 Subject: [PATCH 513/769] Make DNS blocklist URLs configurable (#548) --- config.cfg | 6 +++++ roles/dns_adblocking/tasks/main.yml | 2 +- .../templates/{adblock.sh => adblock.sh.j2} | 25 +++++++++++-------- 3 files changed, 21 insertions(+), 12 deletions(-) rename roles/dns_adblocking/templates/{adblock.sh => adblock.sh.j2} (50%) diff --git a/config.cfg b/config.cfg index 78407bae..0c1e0d36 100644 --- a/config.cfg +++ b/config.cfg @@ -20,6 +20,12 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" +adblock_lists: + - "http://winhelp2002.mvps.org/hosts.txt" + - "https://adaway.org/hosts.txt" + - "https://www.malwaredomainlist.com/hostslist/hosts.txt" + - "https://hosts-file.net/ad_servers.txt" + dns_servers: ipv4: - 8.8.8.8 diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 7e85e0ea..3989bf4f 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -29,7 +29,7 @@ - name: Adblock script created template: - src: adblock.sh + src: adblock.sh.j2 dest: /usr/local/sbin/adblock.sh owner: root group: "{{ root_group|default('root') }}" diff --git a/roles/dns_adblocking/templates/adblock.sh b/roles/dns_adblocking/templates/adblock.sh.j2 similarity index 50% rename from roles/dns_adblocking/templates/adblock.sh rename to roles/dns_adblocking/templates/adblock.sh.j2 index 864e35ef..23565645 100644 --- a/roles/dns_adblocking/templates/adblock.sh +++ b/roles/dns_adblocking/templates/adblock.sh.j2 @@ -7,36 +7,39 @@ ENDPOINT_IP6="::" IPV6="Y" TEMP=`mktemp` TEMP_SORTED=`mktemp` +DNSMASQ_WHITELIST="/var/lib/dnsmasq/white.list" +DNSMASQ_BLACKLIST="/var/lib/dnsmasq/black.list" +DNSMASQ_BLOCKHOSTS="/var/lib/dnsmasq/block.hosts" +BLOCKLIST_URLS="{% for url in adblock_lists %}{{ url }} {% endfor %}" #Delete the old block.hosts to make room for the updates -rm -f /var/lib/dnsmasq/block.hosts +rm -f $DNSMASQ_BLOCKHOSTS echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) -wget -qO- http://winhelp2002.mvps.org/hosts.txt| awk -v r="$ENDPOINT_IP4" '{sub(/^0.0.0.0/, r)} $0 ~ "^"r' > "$TEMP" -wget -qO- "https://adaway.org/hosts.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" -wget -qO- https://www.malwaredomainlist.com/hostslist/hosts.txt|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" -wget -qO- "https://hosts-file.net/.\ad_servers.txt"|awk -v r="$ENDPOINT_IP4" '{sub(/^127.0.0.1/, r)} $0 ~ "^"r' >> "$TEMP" +for url in $BLOCKLIST_URLS; do + wget -qO- "$url" | awk -v r="$ENDPOINT_IP4" '{sub(/^(0.0.0.0|127.0.0.1)/, r)} $0 ~ "^"r' >> "$TEMP" +done #Add black list, if non-empty -if [ -s "/var/lib/dnsmasq/black.list" ] +if [ -s "$DNSMASQ_BLACKLIST" ] then echo 'Adding blacklist...' - awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' /var/lib/dnsmasq/black.list >> "$TEMP" + awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' $DNSMASQ_BLACKLIST >> "$TEMP" fi #Sort the download/black lists awk '{sub(/\r$/,"");print $1,$2}' "$TEMP"|sort -u > "$TEMP_SORTED" #Filter (if applicable) -if [ -s "/var/lib/dnsmasq/white.list" ] +if [ -s "$DNSMASQ_WHITELIST" ] then #Filter the blacklist, suppressing whitelist matches # This is relatively slow =-( echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" /var/lib/dnsmasq/white.list | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > /var/lib/dnsmasq/block.hosts + egrep -v "^[[:space:]]*$" $DNSMASQ_WHITELIST | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS else - cat "$TEMP_SORTED" > /var/lib/dnsmasq/block.hosts + cat "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS fi if [ "$IPV6" = "Y" ] @@ -44,7 +47,7 @@ then safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') echo 'Adding ipv6 support...' - sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" /var/lib/dnsmasq/block.hosts + sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" $DNSMASQ_BLOCKHOSTS fi service dnsmasq restart From e9e6c6e383e5e6c235af6be61a5cbe3d76fec901 Mon Sep 17 00:00:00 2001 From: Ruben Jongejan Date: Wed, 17 May 2017 08:30:04 +0200 Subject: [PATCH 514/769] cleaner syntax for local actions (#536) * refactored local actions to cleaner syntax * openssl commands folded * removed unnecessary local_action's --- playbooks/local.yml | 13 +- playbooks/local_ssh.yml | 8 +- playbooks/post.yml | 3 +- roles/vpn/tasks/client_configs.yml | 21 +-- roles/vpn/tasks/main.yml | 1 + roles/vpn/tasks/openssl.yml | 268 ++++++++++++++++------------- 6 files changed, 170 insertions(+), 144 deletions(-) diff --git a/playbooks/local.yml b/playbooks/local.yml index a7cc2d7e..be2ecc9f 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -1,16 +1,23 @@ --- - name: Generate the SSH private key - local_action: shell echo -e 'n' | ssh-keygen -b 2048 -C {{ SSH_keys.comment }} -t rsa -f {{ SSH_keys.private }} -q -N "" + shell: > + echo -e 'n' | + ssh-keygen -b 2048 -C {{ SSH_keys.comment }} + -t rsa -f {{ SSH_keys.private }} -q -N "" args: creates: "{{ SSH_keys.private }}" - name: Generate the SSH public key - local_action: shell echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} > {{ SSH_keys.public }} + shell: > + echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} + > {{ SSH_keys.public }} changed_when: false - name: Change mode for the SSH private key - local_action: file path={{ SSH_keys.private }} mode=0600 + file: + path: "{{ SSH_keys.private }}" + mode: 0600 - name: Ensure the dynamic inventory exists blockinfile: diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml index 05e53d9a..b2b30b77 100644 --- a/playbooks/local_ssh.yml +++ b/playbooks/local_ssh.yml @@ -1,14 +1,12 @@ --- - name: Ensure the local ssh directory is exist - local_action: - module: file - path: "~/.ssh/" + file: + path: ~/.ssh/ state: directory - name: Copy the algo ssh key to the local ssh directory - local_action: - module: copy + copy: src: "{{ SSH_keys.private }}" dest: ~/.ssh/algo.pem mode: '0600' diff --git a/playbooks/post.yml b/playbooks/post.yml index 3f1c1423..f9f41983 100644 --- a/playbooks/post.yml +++ b/playbooks/post.yml @@ -1,8 +1,7 @@ --- - name: Wait until SSH becomes ready... - local_action: - module: wait_for + wait_for: port: 22 host: "{{ cloud_instance_ip }}" search_regex: "OpenSSH" diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index 76f5a05a..227a2a1a 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -1,8 +1,7 @@ --- - name: Register p12 PayloadContent - local_action: > - shell cat private/{{ item }}.p12 | base64 + shell: cat private/{{ item }}.p12 | base64 register: PayloadContent become: no args: @@ -15,8 +14,7 @@ PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - name: Build the mobileconfigs - local_action: - module: template + template: src: mobileconfig.j2 dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig mode: 0600 @@ -27,8 +25,7 @@ no_log: True - name: Build the strongswan app android config - local_action: - module: template + template: src: sswan.j2 dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.sswan mode: 0600 @@ -39,8 +36,7 @@ no_log: True - name: Build the client ipsec config file - local_action: - module: template + template: src: client_ipsec.conf.j2 dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf mode: 0600 @@ -49,8 +45,7 @@ - "{{ users }}" - name: Build the client ipsec secret file - local_action: - module: template + template: src: client_ipsec.secrets.j2 dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets mode: 0600 @@ -59,8 +54,7 @@ - "{{ users }}" - name: Build the windows client powershell script - local_action: - module: template + template: src: client_windows.ps1.j2 dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 mode: 0600 @@ -69,8 +63,7 @@ with_items: "{{ users }}" - name: Restrict permissions for the local private directories - local_action: - module: file + file: path: "{{ item }}" state: directory mode: 0700 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 9a9c9277..33b70de1 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -24,6 +24,7 @@ tags: update-users - include: distribute_keys.yml - include: client_configs.yml + delegate_to: localhost tags: update-users - meta: flush_handlers diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 44fd9086..23cde5a0 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -1,141 +1,169 @@ --- -- name: Ensure the pki directory does not exist - local_action: - module: file - dest: configs/{{ IP_subject_alt_name }}/pki - state: absent - become: no - when: easyrsa_reinit_existent == True +- block: + - name: Ensure the pki directory does not exist + file: + dest: configs/{{ IP_subject_alt_name }}/pki + state: absent + when: easyrsa_reinit_existent == True -- name: Ensure the pki directories exist - local_action: - module: file - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" - state: directory - recurse: yes - become: no - with_items: - - ecparams - - certs - - crl - - newcerts - - private - - reqs + - name: Ensure the pki directories exist + file: + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: directory + recurse: yes + with_items: + - ecparams + - certs + - crl + - newcerts + - private + - reqs -- name: Ensure the files exist - local_action: - module: file - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" - state: touch - become: no - with_items: - - ".rnd" - - "private/.rnd" - - "index.txt" - - "index.txt.attr" - - "serial" + - name: Ensure the files exist + file: + dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + state: touch + with_items: + - ".rnd" + - "private/.rnd" + - "index.txt" + - "index.txt.attr" + - "serial" -- name: Generate the openssl server configs - local_action: - module: template - src: openssl.cnf.j2 - dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" - become: no + - name: Generate the openssl server configs + template: + src: openssl.cnf.j2 + dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" -- name: Build the CA pair - local_action: > - shell openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && - openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch -passout pass:"{{ easyrsa_CA_password }}" && + - name: Build the CA pair + shell: > + openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && + openssl req -utf8 -new + -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -config openssl.cnf + -keyout private/cakey.pem + -out cacert.pem -x509 -days 3650 + -batch + -passout pass:"{{ easyrsa_CA_password }}" && touch {{ IP_subject_alt_name }}_ca_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: "{{ IP_subject_alt_name }}_ca_generated" - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: "{{ IP_subject_alt_name }}_ca_generated" + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" -- name: Copy the CA certificate - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" - mode: 0600 - become: no + - name: Copy the CA certificate + copy: + src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" + mode: 0600 -- name: Generate the serial number - local_action: > - shell echo 01 > serial && - touch serial_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: serial_generated + - name: Generate the serial number + shell: echo 01 > serial && touch serial_generated + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: serial_generated -- name: Build the server pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && - openssl ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && - touch certs/{{ IP_subject_alt_name }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ IP_subject_alt_name }}_crt_generated - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + - name: Build the server pair + shell: > + openssl req -utf8 -new + -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -config openssl.cnf + -keyout private/{{ IP_subject_alt_name }}.key + -out reqs/{{ IP_subject_alt_name }}.req -nodes + -passin pass:"{{ easyrsa_CA_password }}" + -subj "/CN={{ IP_subject_alt_name }}" -batch && + openssl ca -utf8 + -in reqs/{{ IP_subject_alt_name }}.req + -out certs/{{ IP_subject_alt_name }}.crt + -config openssl.cnf -days 3650 -batch + -passin pass:"{{ easyrsa_CA_password }}" + -subj "/CN={{ IP_subject_alt_name }}" && + touch certs/{{ IP_subject_alt_name }}_crt_generated + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ IP_subject_alt_name }}_crt_generated + environment: + subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" -- name: Build the client's pair - local_action: > - shell openssl req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} -config openssl.cnf -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && - openssl ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config openssl.cnf -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && + - name: Build the client's pair + shell: > + openssl req -utf8 -new + -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -config openssl.cnf + -keyout private/{{ item }}.key + -out reqs/{{ item }}.req -nodes + -passin pass:"{{ easyrsa_CA_password }}" + -subj "/CN={{ item }}" -batch && + openssl ca -utf8 + -in reqs/{{ item }}.req + -out certs/{{ item }}.crt + -config openssl.cnf -days 3650 -batch + -passin pass:"{{ easyrsa_CA_password }}" + -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: certs/{{ item }}_crt_generated - environment: - subjectAltName: "DNS:{{ item }}" - with_items: "{{ users }}" + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: certs/{{ item }}_crt_generated + environment: + subjectAltName: "DNS:{{ item }}" + with_items: "{{ users }}" -- name: Build the client's p12 - local_action: > - shell openssl pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export -name {{ item }} -out private/{{ item }}.p12 -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" + - name: Build the client's p12 + shell: > + openssl pkcs12 + -in certs/{{ item }}.crt + -inkey private/{{ item }}.key + -export + -name {{ item }} + -out private/{{ item }}.p12 + -certfile cacert.pem + -passout pass:"{{ easyrsa_p12_export_password }}" + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" -- name: Copy the p12 certificates - local_action: - module: copy - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 - become: no - with_items: - - "{{ users }}" + - name: Copy the p12 certificates + copy: + src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" + mode: 0600 + with_items: + - "{{ users }}" -- name: Get active users - local_action: > - shell grep ^V index.txt | grep -v "{{ IP_subject_alt_name }}" | awk '{print $5}' | sed 's/\/CN=//g' - become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - register: valid_certs + - name: Get active users + shell: > + grep ^V index.txt | + grep -v "{{ IP_subject_alt_name }}" | + awk '{print $5}' | + sed 's/\/CN=//g' + args: + chdir: "configs/{{ IP_subject_alt_name }}/pki/" + register: valid_certs -- name: Revoke non-existing users - local_action: > - shell openssl ca -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt && - openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt + - name: Revoke non-existing users + shell: > + openssl ca + -config openssl.cnf + -passin pass:"{{ easyrsa_CA_password }}" + -revoke certs/{{ item }}.crt && + openssl ca -gencrl + -config openssl.cnf + -passin pass:"{{ easyrsa_CA_password }}" + -revoke certs/{{ item }}.crt + -out crl/{{ item }}.crt touch crl/{{ item }}_revoked + args: + chdir: configs/{{ IP_subject_alt_name }}/pki/ + creates: crl/{{ item }}_revoked + environment: + subjectAltName: "DNS:{{ item }}" + when: item not in users + with_items: "{{ valid_certs.stdout_lines }}" + + delegate_to: localhost become: no - args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: crl/{{ item }}_revoked - environment: - subjectAltName: "DNS:{{ item }}" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" - name: Copy the revoked certificates to the vpn server copy: From 40e0363b184e38059487deab580eedea8d3537f0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 22 May 2017 04:27:53 +0200 Subject: [PATCH 515/769] Add html helper for Android (#554) * add html helper #280 move to the new local schema fix a typo * Update client-android.md --- docs/client-android.md | 9 +++++++++ roles/vpn/tasks/client_configs.yml | 12 +++++++++++- roles/vpn/templates/android_html_helper.j2 | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 roles/vpn/templates/android_html_helper.j2 diff --git a/docs/client-android.md b/docs/client-android.md index bd71dddb..91c85fcc 100644 --- a/docs/client-android.md +++ b/docs/client-android.md @@ -1,5 +1,14 @@ # Android client setup +## Installation via profiles + +1. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android). +2. Copy `android_{username}.sswan` and `android_{username}_helper.html` to your phone's internal storage. +3. Open the helper file in a browser (e.g., Google Chrome). +4. Click on the link. It opens the StrongSwan app and configures the VPN with your profile. + +## Manual installation + **NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [strongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. | Instruction | Screenshot(s) | diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index 227a2a1a..ac92f822 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -27,7 +27,7 @@ - name: Build the strongswan app android config template: src: sswan.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.sswan + dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}.sswan mode: 0600 become: no with_together: @@ -35,6 +35,16 @@ - "{{ PayloadContent.results }}" no_log: True +- name: Build the android helper html + template: + src: android_html_helper.j2 + dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}_helper.html + mode: 0600 + become: no + with_together: + - "{{ users }}" + no_log: True + - name: Build the client ipsec config file template: src: client_ipsec.conf.j2 diff --git a/roles/vpn/templates/android_html_helper.j2 b/roles/vpn/templates/android_html_helper.j2 new file mode 100644 index 00000000..d27528aa --- /dev/null +++ b/roles/vpn/templates/android_html_helper.j2 @@ -0,0 +1 @@ +{{ item.0 }} From ee6db3742854b8c6320ec1d597297370eb36245c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 22 May 2017 04:28:18 +0200 Subject: [PATCH 516/769] Change the P12 and SSH passwords only for new users (#550) --- roles/ssh_tunneling/tasks/main.yml | 3 ++- roles/vpn/tasks/openssl.yml | 2 ++ users.yml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 35161bca..8a1d4965 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -37,6 +37,7 @@ ssh_key_bits: 256 ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" + update_password: on_create state: present append: yes with_items: "{{ users }}" @@ -82,7 +83,7 @@ become: no with_items: - "{{ users }}" - + - name: SSH | Get active system users shell: > getent group algo | cut -f4 -d: | sed "s/,/\n/g" diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 23cde5a0..542fec36 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -122,7 +122,9 @@ -passout pass:"{{ easyrsa_p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" + creates: private/{{ item }}.p12 with_items: "{{ users }}" + register: p12 - name: Copy the p12 certificates copy: diff --git a/users.yml b/users.yml index a9be55e6..bf25b035 100644 --- a/users.yml +++ b/users.yml @@ -62,7 +62,7 @@ - debug: msg: - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" + - " {% if p12.changed %}{{ congrats.p12_pass }}{% endif %}" tags: always rescue: - debug: var=fail_hint From 4165eca4078e893e3354ea7a4494eb65e3047bde Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 22 May 2017 17:16:00 +0200 Subject: [PATCH 517/769] Azure supports 17.04 #449 --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 0c1e0d36..18654b60 100644 --- a/config.cfg +++ b/config.cfg @@ -70,7 +70,7 @@ cloud_providers: image: offer: UbuntuServer publisher: Canonical - sku: '16.04-LTS' # 16.04-LTS + sku: '16.04-LTS' # 16.04-LTS / 17.04 version: latest digitalocean: size: 512mb From 97248fce19f78b080f4477ef9f301dbbdcf46594 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 23 May 2017 11:30:26 -0400 Subject: [PATCH 518/769] Default to DigitalOcean rather than AWS for the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ef47570..89c0992c 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Use the example command below to start an SSH tunnel by replacing `user` and `ip To SSH into the Algo server for administrative purposes you can use the example command below by replacing `ip` with your own: - `ssh ubuntu@ip -i ~/.ssh/algo.pem` + `ssh root@ip -i ~/.ssh/algo.pem` If you find yourself regularly logging into Algo then it will be useful to load your Algo ssh key automatically. Add the following snippet to the bottom of `~/.bash_profile` to add it to your shell environment permanently. From e6c8f19d3c5c2e18f07e55dbc8a1ada84533b88a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 May 2017 17:30:57 +0200 Subject: [PATCH 519/769] Create a VPC network for each instane (#561) --- roles/cloud-gce/tasks/main.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index bbd9ef2e..8737a7e7 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -10,6 +10,18 @@ service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + - name: Network configured + gce_net: + name: "algo-{{ server_name }}" + fwname: "algo-{{ server_name }}-fw" + allowed: "udp:500,4500;tcp:22;icmp" + state: "present" + mode: auto + src_range: 0.0.0.0/0 + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ credentials_file_lookup.project_id }}" + - name: "Creating a new instance..." gce: instance_names: "{{ server_name }}" @@ -20,7 +32,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - # ip_forward: true + network: "algo-{{ server_name }}" tags: - "environment-algo" register: google_vm @@ -35,18 +47,6 @@ cloud_provider: gce ipv6_support: no - - name: Firewall configured - local_action: - module: gce_net - name: "{{ google_vm.instance_data[0].network }}" - fwname: "algo-ikev2" - allowed: "udp:500,4500;tcp:22" - state: "present" - src_range: 0.0.0.0/0 - service_account_email: "{{ credentials_file_lookup.client_email }}" - credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" - - set_fact: cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" From 0131505195eacbdb224e6ac660b69b91c653e5f6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 May 2017 17:31:53 +0200 Subject: [PATCH 520/769] Enhance PS1 script (#510) update docs Update README.md update readme --- README.md | 27 +++++------------------ docs/client-windows.md | 27 +++++++++++++++++++++++ roles/vpn/templates/client_windows.ps1.j2 | 21 +++++++++++++++--- 3 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 docs/client-windows.md diff --git a/README.md b/README.md index 89c0992c..3a8d6940 100644 --- a/README.md +++ b/README.md @@ -102,31 +102,16 @@ No version of Android supports IKEv2. Install the [strongSwan VPN Client for And ### Windows -Windows clients have a more complicated setup than most others. Follow the steps below to set one up: +#### Scripted installation -1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. -2. Import the CA certificate to the local machine Trusted Root certificate store. -3. Open PowerShell as Administrator. Navigate to your copied files. -4. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. - -```powershell -Set-ExecutionPolicy Unrestricted -Scope CurrentUser +Copy your powershell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows machine and run the following command as Administrator to configure the VPN connection. +``` +powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 Add ``` -5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. -6. After you execute the user script, set the Execution Policy back before you close the PowerShell window. +#### Manual installation -```powershell -Set-ExecutionPolicy Restricted -Scope CurrentUser -``` - -Your VPN is now installed and ready to use. - -If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: - -```powershell -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -``` +See the [Windows setup instructions](/docs/client-windows.md) for more a more detailed walkthrough. ### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) diff --git a/docs/client-windows.md b/docs/client-windows.md new file mode 100644 index 00000000..1013585c --- /dev/null +++ b/docs/client-windows.md @@ -0,0 +1,27 @@ +# Windows client manual setup + +Windows clients have a more complicated setup than most others. Follow the steps below to set one up: + +1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. +2. Import the CA certificate to the local machine Trusted Root certificate store. +3. Open PowerShell as Administrator. Navigate to your copied files. +4. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. + +```powershell +Set-ExecutionPolicy Unrestricted -Scope CurrentUser +``` + +5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. +6. After you execute the user script, set the Execution Policy back before you close the PowerShell window. + +```powershell +Set-ExecutionPolicy Restricted -Scope CurrentUser +``` + +Your VPN is now installed and ready to use. + +If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: + +```powershell +Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 +``` diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 78201de2..81baa6b2 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,3 +1,18 @@ -certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 -Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 + +function AddAlgoVPN { + certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 + Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required + Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -Force +} + +function RemoveAlgoVPN { + Get-ChildItem cert:LocalMachine/Root | Where-Object { $_.Subject -match '^CN={{ IP_subject_alt_name }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item + Get-ChildItem cert:LocalMachine/My | Where-Object { $_.Subject -match '^CN={{ item }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item + Remove-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -Force +} + +switch ($args[0]) { + "Add" { AddAlgoVPN } + "Remove" { RemoveAlgoVPN } + default { Write-Host Usage: $MyInvocation.MyCommand.Name "(Add|Remove)" } +} From 695f9936a0b3f9892757f023ba76a3c91029d0bd Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 23 May 2017 11:33:46 -0400 Subject: [PATCH 521/769] Update README.md --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a8d6940..614203b0 100644 --- a/README.md +++ b/README.md @@ -102,16 +102,12 @@ No version of Android supports IKEv2. Install the [strongSwan VPN Client for And ### Windows -#### Scripted installation - -Copy your powershell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows machine and run the following command as Administrator to configure the VPN connection. +Copy your PowerShell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows client and run the following command as Administrator to configure the VPN connection. ``` powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 Add ``` -#### Manual installation - -See the [Windows setup instructions](/docs/client-windows.md) for more a more detailed walkthrough. +For a manual installation, see the [Windows setup instructions](/docs/client-windows.md). ### Linux Network Manager Clients (e.g., Ubuntu, Debian, or Fedora Desktop) From e13a76d1f358318dbebadd57a12334bc8d10ad05 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Tue, 23 May 2017 11:36:04 -0400 Subject: [PATCH 522/769] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 614203b0..be539f21 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Certificates and configuration files that users will need are placed in the `con No version of Android supports IKEv2. Install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more a more detailed walkthrough. -### Windows +### Windows 10 Copy your PowerShell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows client and run the following command as Administrator to configure the VPN connection. ``` @@ -168,7 +168,6 @@ If you find yourself regularly logging into Algo then it will be useful to load `ssh-add ~/.ssh/algo > /dev/null 2>&1` - ## Adding or Removing Users If you chose the save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. From f52eca39c3b2b5bfe58f39f5e3842623aee834b4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 May 2017 18:26:01 +0200 Subject: [PATCH 523/769] add some debug to the tests --- tests/update-users.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/update-users.sh b/tests/update-users.sh index b0cbb192..4415961e 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -ex CAPW=`cat /tmp/ca_password` From a9a6933c76ca5e5992aab622ec1dee54aa0e377e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 23 May 2017 18:26:48 +0200 Subject: [PATCH 524/769] a typo --- tests/update-users.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/update-users.sh b/tests/update-users.sh index 4415961e..8777c82a 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -20,7 +20,7 @@ fi if openssl x509 -inform pem -noout -text -in certs/jack_test.crt | grep CN=jack_test then - echo "The new user exist" + echo "The new user exists" else echo "The new user does not exist" exit 1 From d59d67f0ea2aa3f4473b8c9bf82d30ae5ac5dd48 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Sat, 27 May 2017 08:22:05 -0400 Subject: [PATCH 525/769] Add additional Gloud Cloud Engine zones. (#569) * Add additional Gloud Cloud Engine zones. Add GCE zones for Northern Virginia (us-east4), Singapore (asia-southeast1), and Tokyo (asia-northeast1) regions. * Update possible GCE zones in documentation. --- algo | 52 ++++++++++++++++++++++++------------- docs/deploy-from-ansible.md | 10 +++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/algo b/algo index 551dbe4d..d68b49d0 100755 --- a/algo +++ b/algo @@ -310,15 +310,23 @@ Name the vpn server: 4. Central US (Iowa B) 5. Central US (Iowa C) 6. Central US (Iowa F) - 7. Eastern US (South Carolina B) - 8. Eastern US (South Carolina C) - 9. Eastern US (South Carolina D) - 10. Western Europe (Belgium B) - 11. Western Europe (Belgium C) - 12. Western Europe (Belgium D) - 13. East Asia (Taiwan A) - 14. East Asia (Taiwan B) - 15. East Asia (Taiwan C) + 7. Eastern US (Northern Virginia A) + 8. Eastern US (Northern Virginia B) + 9. Eastern US (Northern Virginia C) + 10. Eastern US (South Carolina B) + 11. Eastern US (South Carolina C) + 12. Eastern US (South Carolina D) + 13. Western Europe (Belgium B) + 14. Western Europe (Belgium C) + 15. Western Europe (Belgium D) + 16. Southeast Asia (Singapore A) + 17. Southeast Asia (Singapore B) + 18. East Asia (Taiwan A) + 19. East Asia (Taiwan B) + 20. East Asia (Taiwan C) + 21. Northeast Asia (Tokyo A) + 22. Northeast Asia (Tokyo B) + 23. Northeast Asia (Tokyo C) Please choose the number of your zone. Press enter for default (#8) zone. [8]: " -r region region=${region:-8} @@ -330,15 +338,23 @@ Please choose the number of your zone. Press enter for default (#8) zone. 4) zone="us-central1-b" ;; 5) zone="us-central1-c" ;; 6) zone="us-central1-f" ;; - 7) zone="us-east1-b" ;; - 8) zone="us-east1-c" ;; - 9) zone="us-east1-d" ;; - 10) zone="europe-west1-b" ;; - 11) zone="europe-west1-c" ;; - 12) zone="europe-west1-d" ;; - 13) zone="asia-east1-a" ;; - 14) zone="asia-east1-b" ;; - 15) zone="asia-east1-c" ;; + 7) zone="us-east4-a" ;; + 8) zone="us-east4-b" ;; + 9) zone="us-east4-c" ;; + 10) zone="us-east1-b" ;; + 11) zone="us-east1-c" ;; + 12) zone="us-east1-d" ;; + 13) zone="europe-west1-b" ;; + 14) zone="europe-west1-c" ;; + 15) zone="europe-west1-d" ;; + 16) zone="asia-southeast1-a" ;; + 17) zone="asia-southeast1-b" ;; + 18) zone="asia-east1-a" ;; + 19) zone="asia-east1-b" ;; + 20) zone="asia-east1-c" ;; + 21) zone="asia-northeast1-a" ;; + 22) zone="asia-northeast1-b" ;; + 23) zone="asia-northeast1-c" ;; esac ROLES="gce vpn cloud" diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 46a3cdfa..6f6bdca3 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -180,16 +180,26 @@ Required variables: Possible options for `zone`: +- us-west1-a +- us-west1-b - us-central1-a - us-central1-b - us-central1-c - us-central1-f +- us-east4-a +- us-east4-b +- us-east4-c - us-east1-b - us-east1-c - us-east1-d - europe-west1-b - europe-west1-c - europe-west1-d +- asia-southeast1-a +- asia-southeast1-b - asia-east1-a - asia-east1-b - asia-east1-c +- asia-northeast1-a +- asia-northeast1-b +- asia-northeast1-c From 87e1282ebbb1044d371b95d72372bf024ec9ebde Mon Sep 17 00:00:00 2001 From: Christopher De Vries Date: Wed, 31 May 2017 05:56:17 -0700 Subject: [PATCH 526/769] Make documentation on iptables for local installation clearer. (#575) --- docs/deploy-from-ansible.md | 6 ++++++ docs/deploy-to-ubuntu.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 6f6bdca3..0a9cafd3 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -51,6 +51,12 @@ Required variables: - server_user - IP_subject_alt_name +Note that by default, the iptables rules on your existing server will be overwritten. If you don't want to overwrite the iptables rules, you can use the `--skip-tags iptables` flag, for example: + +```shell +ansible-playbook deploy.yml -t local,vpn --skip-tags iptables -e 'server_ip=172.217.2.238 server_user=algo IP_subject_alt_name=172.217.2.238' +``` + ### Digital Ocean Required variables: diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 0d166523..929b7192 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -13,4 +13,4 @@ git clone https://github.com/trailofbits/algo cd algo && ./algo ``` -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described below. +**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). From 6fb52042899bad4a5ae22b8713f52649d7de4a30 Mon Sep 17 00:00:00 2001 From: bhawkins Date: Wed, 31 May 2017 06:02:53 -0700 Subject: [PATCH 527/769] Note different admin usernames (refs trailofbits/algo#557). (#564) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index be539f21..63ba827a 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,8 @@ If you find yourself regularly logging into Algo then it will be useful to load `ssh-add ~/.ssh/algo > /dev/null 2>&1` +Note the admin username is `ubuntu` instead of `root` on providers other than Digital Ocean. + ## Adding or Removing Users If you chose the save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. From 91d9eb8f881c64ce6acd3e1f57302d08d84a8a1a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 31 May 2017 18:03:28 +0200 Subject: [PATCH 528/769] Update deploy-from-windows.md --- docs/deploy-from-windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-windows.md b/docs/deploy-from-windows.md index e261078e..69034992 100644 --- a/docs/deploy-from-windows.md +++ b/docs/deploy-from-windows.md @@ -28,7 +28,7 @@ sudo apt-get update && sudo apt-get install python-pip python-setuptools build-e Clone the Algo repository: ```shell -git clone https://github.com/trailofbits/algo && cd algo +cd ~ && git clone https://github.com/trailofbits/algo && cd algo ``` Now, you can go through the [README](https://github.com/trailofbits/algo#deploy-the-algo-server) (start from the 4th step) and deploy your Algo server! From a20e0eed552661772446353bd212c00da1dd95f3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 31 May 2017 18:06:41 +0200 Subject: [PATCH 529/769] Update deploy-from-windows.md --- docs/deploy-from-windows.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/deploy-from-windows.md b/docs/deploy-from-windows.md index 69034992..33fe1b92 100644 --- a/docs/deploy-from-windows.md +++ b/docs/deploy-from-windows.md @@ -22,7 +22,15 @@ The subsystem will be installed, then Windows will require a reboot. Reboot, the Install additional packages: ```shell -sudo apt-get update && sudo apt-get install python-pip python-setuptools build-essential libssl-dev libffi-dev python-dev python-virtualenv git -y +sudo apt-get update && sudo apt-get install \ + git \ + build-essential \ + libssl-dev \ + libffi-dev \ + python-dev \ + python-pip \ + python-setuptools \ + python-virtualenv -y ``` Clone the Algo repository: From ba7859ba5f757dfea92e685996defdb5bb35a423 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 4 Jun 2017 11:30:55 +0200 Subject: [PATCH 530/769] Revoke non-existing users fix --- roles/vpn/tasks/openssl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 542fec36..313a1330 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -154,7 +154,7 @@ -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt - -out crl/{{ item }}.crt + -out crl/{{ item }}.crt && touch crl/{{ item }}_revoked args: chdir: configs/{{ IP_subject_alt_name }}/pki/ From 26c202ded5acbd4548bcf9b992dac39dc266531c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 4 Jun 2017 18:18:55 +0200 Subject: [PATCH 531/769] Generate p12 each deployment. Generate ps1 scripts if windows supported. Define `become` for all the section. (#580) --- config.cfg | 2 +- roles/vpn/tasks/client_configs.yml | 21 ++++++++++++--------- roles/vpn/tasks/main.yml | 1 + roles/vpn/tasks/openssl.yml | 1 - 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/config.cfg b/config.cfg index 18654b60..869c53c5 100644 --- a/config.cfg +++ b/config.cfg @@ -53,7 +53,7 @@ congrats: "# and ensure that all your traffic passes through the VPN. #" "# Local DNS resolver {{ local_service_ip }} #" p12_pass: | - "# The p12 and SSH keys password is {{ easyrsa_p12_export_password }} #" + "# The p12 and SSH keys password for new users is {{ easyrsa_p12_export_password }} #" ca_key_pass: | "# The CA key password is {{ easyrsa_CA_password }} #" ssh_access: | diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index ac92f822..ea1621a2 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -3,7 +3,6 @@ - name: Register p12 PayloadContent shell: cat private/{{ item }}.p12 | base64 register: PayloadContent - become: no args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" with_items: "{{ users }}" @@ -18,7 +17,6 @@ src: mobileconfig.j2 dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig mode: 0600 - become: no with_together: - "{{ users }}" - "{{ PayloadContent.results }}" @@ -29,7 +27,6 @@ src: sswan.j2 dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}.sswan mode: 0600 - become: no with_together: - "{{ users }}" - "{{ PayloadContent.results }}" @@ -40,7 +37,6 @@ src: android_html_helper.j2 dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}_helper.html mode: 0600 - become: no with_together: - "{{ users }}" no_log: True @@ -50,7 +46,6 @@ src: client_ipsec.conf.j2 dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf mode: 0600 - become: no with_items: - "{{ users }}" @@ -59,17 +54,26 @@ src: client_ipsec.secrets.j2 dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets mode: 0600 - become: no with_items: - "{{ users }}" +- name: Create the windows check file + file: + state: touch + path: configs/{{ IP_subject_alt_name }}/.supports_windows + when: Win10_Enabled is defined and Win10_Enabled == "Y" + +- name: Check if the windows check file exists + stat: + path: configs/{{ IP_subject_alt_name }}/.supports_windows + register: supports_windows + - name: Build the windows client powershell script template: src: client_windows.ps1.j2 dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 mode: 0600 - become: no - when: Win10_Enabled is defined and Win10_Enabled == "Y" + when: Win10_Enabled is defined and Win10_Enabled == "Y" or supports_windows.stat.exists == true with_items: "{{ users }}" - name: Restrict permissions for the local private directories @@ -77,6 +81,5 @@ path: "{{ item }}" state: directory mode: 0700 - become: no with_items: - configs/{{ IP_subject_alt_name }} diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 33b70de1..7fab8bbc 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -25,6 +25,7 @@ - include: distribute_keys.yml - include: client_configs.yml delegate_to: localhost + become: no tags: update-users - meta: flush_handlers diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 313a1330..ed2b9990 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -122,7 +122,6 @@ -passout pass:"{{ easyrsa_p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" - creates: private/{{ item }}.p12 with_items: "{{ users }}" register: p12 From a8ebb16437903180c7235cb16c5211b4be6d90dd Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 5 Jun 2017 17:33:03 +0200 Subject: [PATCH 532/769] Enable timeouts. Fixes #581 --- roles/dns_adblocking/templates/adblock.sh.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_adblocking/templates/adblock.sh.j2 b/roles/dns_adblocking/templates/adblock.sh.j2 index 23565645..28de7435 100644 --- a/roles/dns_adblocking/templates/adblock.sh.j2 +++ b/roles/dns_adblocking/templates/adblock.sh.j2 @@ -18,7 +18,7 @@ rm -f $DNSMASQ_BLOCKHOSTS echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) for url in $BLOCKLIST_URLS; do - wget -qO- "$url" | awk -v r="$ENDPOINT_IP4" '{sub(/^(0.0.0.0|127.0.0.1)/, r)} $0 ~ "^"r' >> "$TEMP" + wget --timeout=2 --tries=3 -qO- "$url" | awk -v r="$ENDPOINT_IP4" '{sub(/^(0.0.0.0|127.0.0.1)/, r)} $0 ~ "^"r' >> "$TEMP" done #Add black list, if non-empty From f0283856ad52780157e5111ee8dc786cdae8e2d8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 6 Jun 2017 12:42:23 +0200 Subject: [PATCH 533/769] fix revocation (#586) --- roles/vpn/tasks/openssl.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index ed2b9990..a1709bc0 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -145,19 +145,14 @@ - name: Revoke non-existing users shell: > - openssl ca - -config openssl.cnf - -passin pass:"{{ easyrsa_CA_password }}" - -revoke certs/{{ item }}.crt && openssl ca -gencrl -config openssl.cnf -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt - -out crl/{{ item }}.crt && - touch crl/{{ item }}_revoked + -out crl/{{ item }}.crt args: chdir: configs/{{ IP_subject_alt_name }}/pki/ - creates: crl/{{ item }}_revoked + creates: crl/{{ item }}.crt environment: subjectAltName: "DNS:{{ item }}" when: item not in users From 220da6eb538366a7c5bd5cd5aa8da45272736060 Mon Sep 17 00:00:00 2001 From: defunct Date: Wed, 7 Jun 2017 00:31:00 -0400 Subject: [PATCH 534/769] Update AWS policy in documentation (#587) Resolves #579 --- docs/deploy-from-ansible.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 0a9cafd3..01376652 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -133,7 +133,6 @@ Additional tags: "Action": [ "cloudformation:CreateStack", "cloudformation:DescribeStacks", - "cloudformation:CreateStacks", "cloudformation:DescribeStackEvents", "cloudformation:ListStackResources" ], From 2f4d0c016addb0915561044f50016a557ce22ced Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Wed, 7 Jun 2017 00:31:22 -0400 Subject: [PATCH 535/769] Add new Google Cloud us-west1-c zone. (#583) Add new Google Cloud zone (see https://cloudplatform.googleblog.com/2017/05/Oregon-region-us-west1-adds-third-zone-Cloud-SQL-and-Regional-Managed-Instance-Groups.html). Restore original default zone (europe-west1-b; see a470bf071e3a0bcb). --- algo | 90 +++++++++++++++++++------------------ docs/deploy-from-ansible.md | 1 + 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/algo b/algo index d68b49d0..77f8fc58 100755 --- a/algo +++ b/algo @@ -306,55 +306,57 @@ Name the vpn server: What zone should the server be located in? 1. Western US (Oregon A) 2. Western US (Oregon B) - 3. Central US (Iowa A) - 4. Central US (Iowa B) - 5. Central US (Iowa C) - 6. Central US (Iowa F) - 7. Eastern US (Northern Virginia A) - 8. Eastern US (Northern Virginia B) - 9. Eastern US (Northern Virginia C) - 10. Eastern US (South Carolina B) - 11. Eastern US (South Carolina C) - 12. Eastern US (South Carolina D) - 13. Western Europe (Belgium B) - 14. Western Europe (Belgium C) - 15. Western Europe (Belgium D) - 16. Southeast Asia (Singapore A) - 17. Southeast Asia (Singapore B) - 18. East Asia (Taiwan A) - 19. East Asia (Taiwan B) - 20. East Asia (Taiwan C) - 21. Northeast Asia (Tokyo A) - 22. Northeast Asia (Tokyo B) - 23. Northeast Asia (Tokyo C) -Please choose the number of your zone. Press enter for default (#8) zone. -[8]: " -r region + 3. Western US (Oregon C) + 4. Central US (Iowa A) + 5. Central US (Iowa B) + 6. Central US (Iowa C) + 7. Central US (Iowa F) + 8. Eastern US (Northern Virginia A) + 9. Eastern US (Northern Virginia B) + 10. Eastern US (Northern Virginia C) + 11. Eastern US (South Carolina B) + 12. Eastern US (South Carolina C) + 13. Eastern US (South Carolina D) + 14. Western Europe (Belgium B) + 15. Western Europe (Belgium C) + 16. Western Europe (Belgium D) + 17. Southeast Asia (Singapore A) + 18. Southeast Asia (Singapore B) + 19. East Asia (Taiwan A) + 20. East Asia (Taiwan B) + 21. East Asia (Taiwan C) + 22. Northeast Asia (Tokyo A) + 23. Northeast Asia (Tokyo B) + 24. Northeast Asia (Tokyo C) +Please choose the number of your zone. Press enter for default (#14) zone. +[14]: " -r region region=${region:-8} case "$region" in 1) zone="us-west1-a" ;; 2) zone="us-west1-b" ;; - 3) zone="us-central1-a" ;; - 4) zone="us-central1-b" ;; - 5) zone="us-central1-c" ;; - 6) zone="us-central1-f" ;; - 7) zone="us-east4-a" ;; - 8) zone="us-east4-b" ;; - 9) zone="us-east4-c" ;; - 10) zone="us-east1-b" ;; - 11) zone="us-east1-c" ;; - 12) zone="us-east1-d" ;; - 13) zone="europe-west1-b" ;; - 14) zone="europe-west1-c" ;; - 15) zone="europe-west1-d" ;; - 16) zone="asia-southeast1-a" ;; - 17) zone="asia-southeast1-b" ;; - 18) zone="asia-east1-a" ;; - 19) zone="asia-east1-b" ;; - 20) zone="asia-east1-c" ;; - 21) zone="asia-northeast1-a" ;; - 22) zone="asia-northeast1-b" ;; - 23) zone="asia-northeast1-c" ;; + 3) zone="us-west1-c" ;; + 4) zone="us-central1-a" ;; + 5) zone="us-central1-b" ;; + 6) zone="us-central1-c" ;; + 7) zone="us-central1-f" ;; + 8) zone="us-east4-a" ;; + 9) zone="us-east4-b" ;; + 10) zone="us-east4-c" ;; + 11) zone="us-east1-b" ;; + 12) zone="us-east1-c" ;; + 13) zone="us-east1-d" ;; + 14) zone="europe-west1-b" ;; + 15) zone="europe-west1-c" ;; + 16) zone="europe-west1-d" ;; + 17) zone="asia-southeast1-a" ;; + 18) zone="asia-southeast1-b" ;; + 19) zone="asia-east1-a" ;; + 20) zone="asia-east1-b" ;; + 21) zone="asia-east1-c" ;; + 22) zone="asia-northeast1-a" ;; + 23) zone="asia-northeast1-b" ;; + 24) zone="asia-northeast1-c" ;; esac ROLES="gce vpn cloud" diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 01376652..abd335b1 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -187,6 +187,7 @@ Possible options for `zone`: - us-west1-a - us-west1-b +- us-west1-c - us-central1-a - us-central1-b - us-central1-c From ae2a2b522e9e3c2a1e8687df3175b42339bb153a Mon Sep 17 00:00:00 2001 From: defunct Date: Wed, 7 Jun 2017 12:18:57 -0400 Subject: [PATCH 536/769] Add UpdateStack to IAM template (#588) Resolves #585 --- docs/deploy-from-ansible.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index abd335b1..7976711f 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -132,6 +132,7 @@ Additional tags: "Effect": "Allow", "Action": [ "cloudformation:CreateStack", + "cloudformation:UpdateStack", "cloudformation:DescribeStacks", "cloudformation:DescribeStackEvents", "cloudformation:ListStackResources" From 3f62dab0401403f822764aa52eadb27f1001bd45 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 7 Jun 2017 19:37:23 +0200 Subject: [PATCH 537/769] addiitonal check #589 --- algo | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/algo b/algo index 77f8fc58..67d95d64 100755 --- a/algo +++ b/algo @@ -448,10 +448,22 @@ Do you want each user to have their own account for SSH tunneling? [y/N]: " -r ssh_tunneling_enabled ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} - read -p " +if [ "x${server_ip}" = "xlocalhost" ]; then + myip="" +else + myip=${server_ip} +fi + +read -p " + Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$server_ip]: " -r IP_subject - IP_subject=${IP_subject:-$server_ip} +[$myip]: " -r IP_subject +IP_subject=${IP_subject:-$myip} + +if [ "x${IP_subject}" = "x" ]; then +echo "no server IP given. exiting." +exit 1 +fi read -p " Enter the password for the private CA key: From 6ae113a38ff9632283ded8705bc61d2f5b86cf89 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 8 Jun 2017 16:27:35 +0200 Subject: [PATCH 538/769] update-users fix (#591) --- algo | 2 +- users.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/algo b/algo index 67d95d64..4ff5fefb 100755 --- a/algo +++ b/algo @@ -470,7 +470,7 @@ Enter the password for the private CA key: $ADDITIONAL_PROMPT : " -rs easyrsa_CA_password -ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" -t update-users --skip-tags common +ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject_alt_name=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" -t update-users --skip-tags common } case "$1" in diff --git a/users.yml b/users.yml index bf25b035..92792085 100644 --- a/users.yml +++ b/users.yml @@ -16,7 +16,7 @@ ansible_python_interpreter: "/usr/bin/python2.7" ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" easyrsa_CA_password: "{{ easyrsa_CA_password }}" - IP_subject: "{{ IP_subject }}" + IP_subject: "{{ IP_subject_alt_name }}" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - name: Wait until SSH becomes ready... From ae2efea3decb23acc718b14366a42c9ce1c0138d Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 10 Jun 2017 16:37:00 +0200 Subject: [PATCH 539/769] Set default value for GCE to mentioned value (14) (#594) --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index 4ff5fefb..6aeb6897 100755 --- a/algo +++ b/algo @@ -330,7 +330,7 @@ Name the vpn server: 24. Northeast Asia (Tokyo C) Please choose the number of your zone. Press enter for default (#14) zone. [14]: " -r region - region=${region:-8} + region=${region:-14} case "$region" in 1) zone="us-west1-a" ;; From be200b33bfbfb8b851b612bb2d6de0e4b5282970 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Sat, 10 Jun 2017 11:28:43 -0500 Subject: [PATCH 540/769] Add a Gitter chat badge to README.md (#598) * Add Gitter badge * Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 63ba827a..eee7e377 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Algo VPN +[![Join the chat at https://gitter.im/trailofbits/algo-support](https://badges.gitter.im/trailofbits/algo-support.svg)](https://gitter.im/trailofbits/algo-support?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) From 3032c55b1f5aaf08d10ecd43b783b20989cfbb52 Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Sat, 10 Jun 2017 11:33:30 -0500 Subject: [PATCH 541/769] Add a Gitter chat badge to README.md (#599) * Add Gitter badge * Create README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eee7e377..1699eb13 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Algo VPN -[![Join the chat at https://gitter.im/trailofbits/algo-support](https://badges.gitter.im/trailofbits/algo-support.svg)](https://gitter.im/trailofbits/algo-support?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/trailofbits/algo](https://badges.gitter.im/trailofbits/algo.svg)](https://gitter.im/trailofbits/algo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) [![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) From fa466caeb24b39c5f707debdbb31437583aff4ab Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 19 Jun 2017 11:33:42 -0400 Subject: [PATCH 542/769] Modify guidance --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9a2bc0c8..a862ac26 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -217,4 +217,4 @@ It is possible that the IKE_AUTH payload is too big to fit in a single IP datagr ## I have a problem not covered here -If you have an issue that you cannot solve with the guidance here, [file an issue](https://github.com/trailofbits/algo/issues/new) that describes the problem and we'll do our best to help you. You can also [join our Slack](https://empireslacking.herokuapp.com/) and ask for help in the **#algo-support** channel. +If you have an issue that you cannot solve with the guidance here, [join our Gitter](https://gitter.im/trailofbits/algo) and ask for help. If you think you found a new issue in Algo, [file an issue](https://github.com/trailofbits/algo/issues/new). From 91c375d63fa573ec4c165853d5473b381b8b17fe Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 21 Jun 2017 17:12:20 +0200 Subject: [PATCH 543/769] Update cloud-azure.md --- docs/cloud-azure.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/cloud-azure.md b/docs/cloud-azure.md index cb9c20c1..ae836815 100644 --- a/docs/cloud-azure.md +++ b/docs/cloud-azure.md @@ -45,14 +45,14 @@ or just pass those values to the Algo script [step11-screen]: http://i.imgur.com/NUJ6k7i.jpg [step12-screen]: http://i.imgur.com/VZv5qwb.jpg -[step2-thumb]: http://i.imgur.com/ENvSupEm.png -[step3-thumb]: http://i.imgur.com/sPLQaQem.jpg -[step4-thumb]: http://i.imgur.com/di3xFCMm.jpg -[step5-thumb]: http://i.imgur.com/SipQyRAm.jpg -[step6-thumb]: http://i.imgur.com/RRTqV7Cm.jpg -[step7-thumb]: http://i.imgur.com/ZnqJeVvm.jpg -[step8-thumb]: http://i.imgur.com/WAS8Ovlm.png -[step9-thumb]: http://i.imgur.com/IvTN7o1m.jpg -[step10-thumb]: http://i.imgur.com/j6dgo75m.png -[step11-thumb]: http://i.imgur.com/NUJ6k7im.jpg -[step12-thumb]: http://i.imgur.com/VZv5qwbm.jpg +[step2-thumb]: https://i.imgur.com/ENvSupEm.png +[step3-thumb]: https://i.imgur.com/sPLQaQem.jpg +[step4-thumb]: https://i.imgur.com/di3xFCMm.jpg +[step5-thumb]: https://i.imgur.com/SipQyRAm.jpg +[step6-thumb]: https://i.imgur.com/RRTqV7Cm.jpg +[step7-thumb]: https://i.imgur.com/ZnqJeVvm.jpg +[step8-thumb]: https://i.imgur.com/WAS8Ovlm.png +[step9-thumb]: https://i.imgur.com/IvTN7o1m.jpg +[step10-thumb]: https://i.imgur.com/j6dgo75m.png +[step11-thumb]: https://i.imgur.com/NUJ6k7im.jpg +[step12-thumb]: https://i.imgur.com/VZv5qwbm.jpg From 57554933824fe4c74a69cd9677103d8b4c535e0e Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 21 Jun 2017 17:19:21 +0200 Subject: [PATCH 544/769] Update faq.md chloe.re is unavailable anymore. awesome_bot breaks travisci --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index c7bac984..362a0d20 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -15,7 +15,7 @@ No. This project is under active development. We're happy to [accept and fix iss ## Why aren't you using Tor? -The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). +The goal of this project is not to provide anonymity, but to ensure confidentiality of network traffic. Tor introduces new risks that are unsuitable for Algo's intended users. Namely, with Algo, users are in control over the gateway routing their traffic. With Tor, users are at the mercy of [actively](https://www.securityweek2016.tu-darmstadt.de/fileadmin/user_upload/Group_securityweek2016/pets2016/10_honions-sanatinia.pdf) [malicious](https://web.archive.org/web/20150705184539/https://chloe.re/2015/06/20/a-month-with-badonions/) [exit](https://community.fireeye.com/people/archit.mehta/blog/2014/11/18/onionduke-apt-malware-distributed-via-malicious-tor-exit-node) [nodes](https://www.wired.com/2010/06/wikileaks-documents/). ## Why aren't you using Racoon, LibreSwan, or OpenSwan? From 9d8e39f63d532e4b7886087e9393fabaf55c37f4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 21 Jun 2017 19:39:29 +0200 Subject: [PATCH 545/769] Move back to the Xenial repo (#606) --- roles/vpn/tasks/main.yml | 4 ---- roles/vpn/tasks/ubuntu-hacks.yml | 18 ------------------ roles/vpn/tasks/ubuntu.yml | 3 --- roles/vpn/templates/01_strongswan.pref.j2 | 3 --- roles/vpn/templates/01_xenial_aptconf.j2 | 1 - 5 files changed, 29 deletions(-) delete mode 100644 roles/vpn/tasks/ubuntu-hacks.yml delete mode 100644 roles/vpn/templates/01_strongswan.pref.j2 delete mode 100644 roles/vpn/templates/01_xenial_aptconf.j2 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 7fab8bbc..8e732e1d 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -15,10 +15,6 @@ - name: Install strongSwan package: name=strongswan state=present - - name: Get StrongSwan versions - shell: ipsec --versioncode | grep -oE "^U([0-9]*|\.)*" | sed "s/^U\|\.//g" - register: strongswan_version - - include: ipec_configuration.yml - include: openssl.yml tags: update-users diff --git a/roles/vpn/tasks/ubuntu-hacks.yml b/roles/vpn/tasks/ubuntu-hacks.yml deleted file mode 100644 index a64b754c..00000000 --- a/roles/vpn/tasks/ubuntu-hacks.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- - -- name: Configure apt to use the Xenial release by default - template: - src: 01_xenial_aptconf.j2 - dest: /etc/apt/apt.conf.d/01xenial - -- name: Configure packages preferences - template: - src: 01_strongswan.pref.j2 - dest: /etc/apt/preferences.d/01_strongswan.pref - -- name: Configure the Ubuntu Zesty repository - apt_repository: - repo: deb http://mirrors.kernel.org/ubuntu/ zesty main - state: present - filename: 'zesty' - update_cache: yes diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index db046ad4..ccc561b3 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -3,9 +3,6 @@ - set_fact: strongswan_additional_plugins: [] -- include: ubuntu-hacks.yml - when: ansible_distribution_version == "16.04" - - name: Ubuntu | Install strongSwan apt: name: strongswan diff --git a/roles/vpn/templates/01_strongswan.pref.j2 b/roles/vpn/templates/01_strongswan.pref.j2 deleted file mode 100644 index 3249758f..00000000 --- a/roles/vpn/templates/01_strongswan.pref.j2 +++ /dev/null @@ -1,3 +0,0 @@ -Package: *strongswan* -Pin: release n=zesty -Pin-Priority: 9000 diff --git a/roles/vpn/templates/01_xenial_aptconf.j2 b/roles/vpn/templates/01_xenial_aptconf.j2 deleted file mode 100644 index c589ffcd..00000000 --- a/roles/vpn/templates/01_xenial_aptconf.j2 +++ /dev/null @@ -1 +0,0 @@ -APT::Default-Release "xenial"; From 78bd5b017ccf89ea44fb996e6e9370b78efc5ce7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 21 Jun 2017 19:39:54 +0200 Subject: [PATCH 546/769] client fixes (#605) --- deploy_client.yml | 6 +++--- docs/client-linux.md | 4 ++-- roles/client/tasks/main.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deploy_client.yml b/deploy_client.yml index 4a069559..21fd7709 100644 --- a/deploy_client.yml +++ b/deploy_client.yml @@ -8,7 +8,7 @@ add_host: name: "{{ client_ip }}" groups: client-host - ansible_ssh_user: "{{ server_ssh_user }}" + ansible_ssh_user: "{{ ssh_user }}" vpn_user: "{{ vpn_user }}" server_ip: "{{ server_ip }}" @@ -35,7 +35,7 @@ sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 && sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 changed_when: false - when: "'ubuntu' in distribution.stdout" + when: "'ubuntu' in distribution.stdout|lower" - name: Fedora 25 | Install prerequisites raw: > @@ -44,7 +44,7 @@ sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && rpm -ql python2-dnf || dnf install python2-dnf -y changed_when: false - when: "'fedora' in distribution.stdout" + when: "'fedora' in distribution.stdout|lower" roles: - { role: client, tags: ['client'] } diff --git a/docs/client-linux.md b/docs/client-linux.md index d0772cc9..5f536f1f 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -9,13 +9,13 @@ The playbook is `deploy_client.yml` * `client_ip` - The IP address of your client machine (You can use `localhost` in order to deploy locally) * `vpn_user` - The username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) -* `client_ssh_user` - The username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) +* `ssh_user` - The username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) * `server_ip` - The vpn server ip address ### Example: ```shell -ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com server_ssh_user=root' +ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com ssh_user=root' ``` ### Additional options: diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index c75d2faf..68397148 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -39,9 +39,9 @@ create: yes with_items: - dest: "{{ configs_prefix }}/ipsec.conf" - line: "include ipsec.*.conf" + line: "include ipsec.{{ IP_subject_alt_name }}.conf" - dest: "{{ configs_prefix }}/ipsec.secrets" - line: "include ipsec.*.secrets" + line: "include ipsec.{{ IP_subject_alt_name }}.secrets" notify: - restart strongswan @@ -51,10 +51,10 @@ dest: "{{ item.dest }}" with_items: - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ vpn_user }}.crt" - dest: "{{ configs_prefix }}/ipsec.d/certs/{{ IP_subject_alt_name }}_{{ vpn_user }}.crt" + dest: "{{ configs_prefix }}/ipsec.d/certs/{{ vpn_user }}.crt" - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" dest: "{{ configs_prefix }}/ipsec.d/cacerts/{{ IP_subject_alt_name }}.pem" - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ vpn_user }}.key" - dest: "{{ configs_prefix }}/ipsec.d/private/{{ IP_subject_alt_name }}_{{ vpn_user }}.key" + dest: "{{ configs_prefix }}/ipsec.d/private/{{ vpn_user }}.key" notify: - restart strongswan From 2170a8ff25d2504bf4168111f7aee1407f118fe8 Mon Sep 17 00:00:00 2001 From: c Date: Sat, 8 Jul 2017 13:36:00 -0500 Subject: [PATCH 547/769] Add additional GCE zones (London / Australia) (#618) * Update algo * Update algo --- algo | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/algo b/algo index 6aeb6897..99d45c37 100755 --- a/algo +++ b/algo @@ -320,14 +320,20 @@ Name the vpn server: 14. Western Europe (Belgium B) 15. Western Europe (Belgium C) 16. Western Europe (Belgium D) - 17. Southeast Asia (Singapore A) - 18. Southeast Asia (Singapore B) - 19. East Asia (Taiwan A) - 20. East Asia (Taiwan B) - 21. East Asia (Taiwan C) - 22. Northeast Asia (Tokyo A) - 23. Northeast Asia (Tokyo B) - 24. Northeast Asia (Tokyo C) + 17. Western Europe (London A) + 18. Western Europe (London B) + 19. Western Europe (London C) + 20. Southeast Asia (Singapore A) + 21. Southeast Asia (Singapore B) + 22. East Asia (Taiwan A) + 23. East Asia (Taiwan B) + 24. East Asia (Taiwan C) + 25. Northeast Asia (Tokyo A) + 26. Northeast Asia (Tokyo B) + 27. Northeast Asia (Tokyo C) + 28. Australia (Sydney A) + 29. Australia (Sydney B) + 30. Australia (Sydney C) Please choose the number of your zone. Press enter for default (#14) zone. [14]: " -r region region=${region:-14} @@ -349,14 +355,20 @@ Please choose the number of your zone. Press enter for default (#14) zone. 14) zone="europe-west1-b" ;; 15) zone="europe-west1-c" ;; 16) zone="europe-west1-d" ;; - 17) zone="asia-southeast1-a" ;; - 18) zone="asia-southeast1-b" ;; - 19) zone="asia-east1-a" ;; - 20) zone="asia-east1-b" ;; - 21) zone="asia-east1-c" ;; - 22) zone="asia-northeast1-a" ;; - 23) zone="asia-northeast1-b" ;; - 24) zone="asia-northeast1-c" ;; + 17) zone="europe-west2-a" ;; + 18) zone="europe-west2-b" ;; + 19) zone="europe-west2-c" ;; + 20) zone="asia-southeast1-a" ;; + 21) zone="asia-southeast1-b" ;; + 22) zone="asia-east1-a" ;; + 23) zone="asia-east1-b" ;; + 24) zone="asia-east1-c" ;; + 25) zone="asia-northeast1-a" ;; + 26) zone="asia-northeast1-b" ;; + 27) zone="asia-northeast1-c" ;; + 28) zone="australia-southeast1-a" ;; + 29) zone="australia-southeast1-b" ;; + 30) zone="australia-southeast1-c" ;; esac ROLES="gce vpn cloud" @@ -411,7 +423,7 @@ algo_provisioning () { 1. DigitalOcean 2. Amazon EC2 3. Microsoft Azure - 4. Google Compute Engine (only for testing, see issue #369) + 4. Google Compute Engine 5. Install to existing Ubuntu 16.04 server Enter the number of your desired provider From 0bb92790945df438780ec9d50ce091889d97a8c7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 9 Jul 2017 16:32:06 +0200 Subject: [PATCH 548/769] bug in the gce_net module #616 (#620) --- roles/cloud-gce/tasks/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 8737a7e7..ac2bdc01 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -12,9 +12,9 @@ - name: Network configured gce_net: - name: "algo-{{ server_name }}" - fwname: "algo-{{ server_name }}-fw" - allowed: "udp:500,4500;tcp:22;icmp" + name: "algo-net-{{ server_name }}" + fwname: "algo-net-{{ server_name }}-fw" + allowed: "udp:500,4500;tcp:22" state: "present" mode: auto src_range: 0.0.0.0/0 From 0607e968d75ae7cbf9e96131bb45acddbc915ec1 Mon Sep 17 00:00:00 2001 From: Samuel Horwitz Date: Wed, 12 Jul 2017 02:36:43 -0400 Subject: [PATCH 549/769] Update main.yml (#621) --- roles/cloud-gce/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index ac2bdc01..813aa578 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -32,7 +32,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-{{ server_name }}" + network: "algo-net-{{ server_name }}" tags: - "environment-algo" register: google_vm From 95cb34b8bae1df9572f8b897c94d21f3a8a13f60 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sat, 15 Jul 2017 02:10:00 -0400 Subject: [PATCH 550/769] Clear up methods of support even more --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1699eb13..1a6c2298 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,8 @@ # Algo VPN [![Join the chat at https://gitter.im/trailofbits/algo](https://badges.gitter.im/trailofbits/algo.svg)](https://gitter.im/trailofbits/algo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) -[![Slack Status](https://empireslacking.herokuapp.com/badge.svg)](https://empireslacking.herokuapp.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) -[![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) -[![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) -[![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) -[![Bountysource](https://img.shields.io/bountysource/team/trailofbits/activity.svg)](https://www.bountysource.com/teams/trailofbits) +[![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. @@ -200,6 +195,8 @@ After this process completes, the Algo VPN server will contains only the users l * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) +If you read all the documentation and have further questions, [join the chat on Gitter](https://gitter.im/trailofbits/algo). + ## Endorsements > I've been ranting about the sorry state of VPN svcs for so long, probably about @@ -225,6 +222,10 @@ After this process completes, the Algo VPN server will contains only the users l -- [Thorin Klosowski](https://twitter.com/kingthor) for [Lifehacker](http://lifehacker.com/how-to-set-up-your-own-completely-free-vpn-in-the-cloud-1794302432) ## Support Algo VPN +[![Flattr](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo) +[![PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E) +[![Patreon](https://img.shields.io/badge/back_on-patreon-red.svg)](https://www.patreon.com/algovpn) +[![Bountysource](https://img.shields.io/bountysource/team/trailofbits/activity.svg)](https://www.bountysource.com/teams/trailofbits) All donations support continued development. Thanks! From 0a1d64e629748d40157b01d2ffa288ca4a0e0215 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 16 Jul 2017 19:20:41 -0400 Subject: [PATCH 551/769] Update client-linux.md --- docs/client-linux.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/client-linux.md b/docs/client-linux.md index 5f536f1f..89e17e58 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -1,9 +1,6 @@ # Linux client setup -It's possible to deploy an ipsec connection on Linux clients. -Supported distributives are: Debian, Ubuntu, CentOS, Fedora - -The playbook is `deploy_client.yml` +After you deploy a server, you can use an included Ansible script to provision Linux clients too! Debian, Ubuntu, CentOS, and Fedora are supported. The playbook is `deploy_client.yml`. ### Required variables: From 4b02315f1bb959210fcf8ba73b78b0804a8e66fa Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 23 Jul 2017 09:32:14 +0200 Subject: [PATCH 552/769] Disable awesome_bot --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 98ba83b7..f4780b48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ install: - gem install awesome_bot script: - - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new + # - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new # - shellcheck algo # - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check From 8da53f859ba035345fe246028d64c05a8949ab8a Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 23 Jul 2017 20:23:57 +0200 Subject: [PATCH 553/769] Some browsers (eg. Safari) stop loading pages if the element with ads can't be loaded (#633) --- roles/dns_adblocking/tasks/main.yml | 5 ++-- roles/dns_adblocking/templates/adblock.sh.j2 | 20 +++----------- .../dns_adblocking/templates/dnsmasq.conf.j2 | 26 +++++++++---------- 3 files changed, 19 insertions(+), 32 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 3989bf4f..2ba74b77 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -41,11 +41,10 @@ minute: 10 hour: 2 job: /usr/local/sbin/adblock.sh - user: dnsmasq + user: root - name: Update adblock hosts - shell: > - sudo -u dnsmasq "/usr/local/sbin/adblock.sh" + command: /usr/local/sbin/adblock.sh - meta: flush_handlers diff --git a/roles/dns_adblocking/templates/adblock.sh.j2 b/roles/dns_adblocking/templates/adblock.sh.j2 index 28de7435..def11d3a 100644 --- a/roles/dns_adblocking/templates/adblock.sh.j2 +++ b/roles/dns_adblocking/templates/adblock.sh.j2 @@ -1,15 +1,11 @@ #!/bin/sh # Block ads, malware, etc.. -# Redirect endpoint -ENDPOINT_IP4="0.0.0.0" -ENDPOINT_IP6="::" -IPV6="Y" TEMP=`mktemp` TEMP_SORTED=`mktemp` DNSMASQ_WHITELIST="/var/lib/dnsmasq/white.list" DNSMASQ_BLACKLIST="/var/lib/dnsmasq/black.list" -DNSMASQ_BLOCKHOSTS="/var/lib/dnsmasq/block.hosts" +DNSMASQ_BLOCKHOSTS="{{ config_prefix|default('/') }}etc/dnsmasq.d/block.hosts.conf" BLOCKLIST_URLS="{% for url in adblock_lists %}{{ url }} {% endfor %}" #Delete the old block.hosts to make room for the updates @@ -18,18 +14,18 @@ rm -f $DNSMASQ_BLOCKHOSTS echo 'Downloading hosts lists...' #Download and process the files needed to make the lists (enable/add more, if you want) for url in $BLOCKLIST_URLS; do - wget --timeout=2 --tries=3 -qO- "$url" | awk -v r="$ENDPOINT_IP4" '{sub(/^(0.0.0.0|127.0.0.1)/, r)} $0 ~ "^"r' >> "$TEMP" + wget --timeout=2 --tries=3 -qO- "$url" | grep -Ev "(localhost)" | grep -Ew "(0.0.0.0|127.0.0.1)" | awk '{sub(/\r$/,"");print $2}' >> "$TEMP" done #Add black list, if non-empty if [ -s "$DNSMASQ_BLACKLIST" ] then echo 'Adding blacklist...' - awk -v r="$ENDPOINT_IP4" '/^[^#]/ { print r,$1 }' $DNSMASQ_BLACKLIST >> "$TEMP" + cat $DNSMASQ_BLACKLIST >> "$TEMP" fi #Sort the download/black lists -awk '{sub(/\r$/,"");print $1,$2}' "$TEMP"|sort -u > "$TEMP_SORTED" +awk '/^[^#]/ { print "local=/" $1 "/" }' "$TEMP" | sort -u > "$TEMP_SORTED" #Filter (if applicable) if [ -s "$DNSMASQ_WHITELIST" ] @@ -42,14 +38,6 @@ else cat "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS fi -if [ "$IPV6" = "Y" ] -then - safe_pattern=$(printf '%s\n' "$ENDPOINT_IP4" | sed 's/[[\.*^$(){}?+|/]/\\&/g') - safe_addition=$(printf '%s\n' "$ENDPOINT_IP6" | sed 's/[\&/]/\\&/g') - echo 'Adding ipv6 support...' - sed -i -re "s/^(${safe_pattern}) (.*)$/\1 \2\n${safe_addition} \2/g" $DNSMASQ_BLOCKHOSTS -fi - service dnsmasq restart exit 0 diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 026c985e..3b8c4c5c 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -27,8 +27,8 @@ # Replies which are not DNSSEC signed may be legitimate, because the domain # is unsigned, or may be forgeries. Setting this option tells dnsmasq to -# check that an unsigned reply is OK, by finding a secure proof that a DS -# record somewhere between the root and the domain does not exist. +# check that an unsigned reply is OK, by finding a secure proof that a DS +# record somewhere between the root and the domain does not exist. # The cost of setting this is that even queries in unsigned domains will need # one or more extra DNS queries to verify. #dnssec-check-unsigned @@ -130,7 +130,7 @@ bind-interfaces #no-hosts # or if you want it to read another file, as well as /etc/hosts, use # this. -addn-hosts=/var/lib/dnsmasq/block.hosts +# addn-hosts=/var/lib/dnsmasq/block.hosts # Set this (and domain: see below) if you want to have a domain # automatically added to simple names in a hosts-file. @@ -185,11 +185,11 @@ addn-hosts=/var/lib/dnsmasq/block.hosts #dhcp-range=1234::2, 1234::500, 64, 12h # Do Router Advertisements, BUT NOT DHCP for this subnet. -#dhcp-range=1234::, ra-only +#dhcp-range=1234::, ra-only # Do Router Advertisements, BUT NOT DHCP for this subnet, also try and -# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack -# hosts. Use the DHCPv4 lease to derive the name, network segment and +# add names to the DNS for the IPv6 address of SLAAC-configured dual-stack +# hosts. Use the DHCPv4 lease to derive the name, network segment and # MAC address and assume that the host will also have an # IPv6 address calculated using the SLAAC algorithm. #dhcp-range=1234::, ra-names @@ -212,9 +212,9 @@ addn-hosts=/var/lib/dnsmasq/block.hosts #dhcp-range=1234::, ra-stateless, ra-names # Do router advertisements for all subnets where we're doing DHCPv6 -# Unless overridden by ra-stateless, ra-names, et al, the router +# Unless overridden by ra-stateless, ra-names, et al, the router # advertisements will have the M and O bits set, so that the clients -# get addresses and configuration from DHCPv6, and the A bit reset, so the +# get addresses and configuration from DHCPv6, and the A bit reset, so the # clients don't use SLAAC addresses. #enable-ra @@ -287,11 +287,11 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # any machine with Ethernet address starting 11:22:33: #dhcp-host=11:22:33:*:*:*,set:red -# Give a fixed IPv6 address and name to client with +# Give a fixed IPv6 address and name to client with # DUID 00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2 # Note the MAC addresses CANNOT be used to identify DHCPv6 clients. # Note also the they [] around the IPv6 address are obligatory. -#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] +#dhcp-host=id:00:01:00:01:16:d2:83:fc:92:d4:19:e2:d8:b2, fred, [1234::5] # Ignore any clients which are not specified in dhcp-host lines # or /etc/ethers. Equivalent to ISC "deny unknown-clients". @@ -347,7 +347,7 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # Send DHCPv6 option. Note [] around IPv6 addresses. #dhcp-option=option6:dns-server,[1234::77],[1234::88] -# Send DHCPv6 option for nameservers as the machine running +# Send DHCPv6 option for nameservers as the machine running # dnsmasq and another. #dhcp-option=option6:dns-server,[::],[1234::88] @@ -659,11 +659,11 @@ addn-hosts=/var/lib/dnsmasq/block.hosts # Include another lot of configuration options. #conf-file=/etc/dnsmasq.more.conf -#conf-dir=/etc/dnsmasq.d +conf-dir=/etc/dnsmasq.d # Include all the files in a directory except those ending in .bak #conf-dir=/etc/dnsmasq.d,.bak # Include all files in a directory which end in .conf -#conf-dir=/etc/dnsmasq.d/,*.conf +# conf-dir=/etc/dnsmasq.d/,*.conf # From dd43e1e47e0271ed4d4c0a82a747e4f33e1519b5 Mon Sep 17 00:00:00 2001 From: "Paul.W Harvey" Date: Tue, 29 Aug 2017 23:32:12 +1000 Subject: [PATCH 554/769] Use openssl to generate better quality p12_export_password (#655) We're already doing it this way for CA_password, and ansible's to_uuid is problematic as it uses uuid v5 under the hood (#654) --- playbooks/facts/main.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index 7c8516d7..4887bb50 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -27,9 +27,17 @@ become: no register: CA_password +- name: Generate p12 export password + local_action: + module: shell + openssl rand -hex 4 + become: no + register: p12_export_password_generated + when: p12_export_password is not defined + - name: Define password facts set_fact: - easyrsa_p12_export_password: "{{ p12_export_password|default((ansible_date_time.iso8601_basic|sha1|to_uuid).split('-')[0]) }}" + easyrsa_p12_export_password: "{{ p12_export_password|default(p12_export_password_generated.stdout) }}" easyrsa_CA_password: "{{ CA_password.stdout }}" - name: Define the commonName From 80097780128f29a295002d67e9a21b11ff5375fc Mon Sep 17 00:00:00 2001 From: Stev Witzel Date: Tue, 29 Aug 2017 14:32:22 +0100 Subject: [PATCH 555/769] Add new GCP zones in Frankfurt (#656) * add new Frankfurt zones to algo script and ansible docs * backfill ansible docs for recently added GCP zones in London and Sydney --- algo | 50 +++++++++++++++++++++---------------- docs/deploy-from-ansible.md | 9 +++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/algo b/algo index 99d45c37..429a0e39 100755 --- a/algo +++ b/algo @@ -323,17 +323,20 @@ Name the vpn server: 17. Western Europe (London A) 18. Western Europe (London B) 19. Western Europe (London C) - 20. Southeast Asia (Singapore A) - 21. Southeast Asia (Singapore B) - 22. East Asia (Taiwan A) - 23. East Asia (Taiwan B) - 24. East Asia (Taiwan C) - 25. Northeast Asia (Tokyo A) - 26. Northeast Asia (Tokyo B) - 27. Northeast Asia (Tokyo C) - 28. Australia (Sydney A) - 29. Australia (Sydney B) - 30. Australia (Sydney C) + 20. Western Europe (Frankfurt A) + 21. Western Europe (Frankfurt B) + 22. Western Europe (Frankfurt C) + 23. Southeast Asia (Singapore A) + 24. Southeast Asia (Singapore B) + 25. East Asia (Taiwan A) + 26. East Asia (Taiwan B) + 27. East Asia (Taiwan C) + 28. Northeast Asia (Tokyo A) + 29. Northeast Asia (Tokyo B) + 30. Northeast Asia (Tokyo C) + 31. Australia (Sydney A) + 32. Australia (Sydney B) + 33. Australia (Sydney C) Please choose the number of your zone. Press enter for default (#14) zone. [14]: " -r region region=${region:-14} @@ -358,17 +361,20 @@ Please choose the number of your zone. Press enter for default (#14) zone. 17) zone="europe-west2-a" ;; 18) zone="europe-west2-b" ;; 19) zone="europe-west2-c" ;; - 20) zone="asia-southeast1-a" ;; - 21) zone="asia-southeast1-b" ;; - 22) zone="asia-east1-a" ;; - 23) zone="asia-east1-b" ;; - 24) zone="asia-east1-c" ;; - 25) zone="asia-northeast1-a" ;; - 26) zone="asia-northeast1-b" ;; - 27) zone="asia-northeast1-c" ;; - 28) zone="australia-southeast1-a" ;; - 29) zone="australia-southeast1-b" ;; - 30) zone="australia-southeast1-c" ;; + 20) zone="europe-west3-a" ;; + 21) zone="europe-west3-b" ;; + 22) zone="europe-west3-c" ;; + 23) zone="asia-southeast1-a" ;; + 24) zone="asia-southeast1-b" ;; + 25) zone="asia-east1-a" ;; + 26) zone="asia-east1-b" ;; + 27) zone="asia-east1-c" ;; + 28) zone="asia-northeast1-a" ;; + 29) zone="asia-northeast1-b" ;; + 30) zone="asia-northeast1-c" ;; + 31) zone="australia-southeast1-a" ;; + 32) zone="australia-southeast1-b" ;; + 33) zone="australia-southeast1-c" ;; esac ROLES="gce vpn cloud" diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 7976711f..5c92a32b 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -202,6 +202,12 @@ Possible options for `zone`: - europe-west1-b - europe-west1-c - europe-west1-d +- europe-west2-a +- europe-west2-b +- europe-west2-c +- europe-west3-a +- europe-west3-b +- europe-west3-c - asia-southeast1-a - asia-southeast1-b - asia-east1-a @@ -210,3 +216,6 @@ Possible options for `zone`: - asia-northeast1-a - asia-northeast1-b - asia-northeast1-c +- australia-southeast1-a +- australia-southeast1-b +- australia-southeast1-c From 9582cba128f8daa9981dacc340f84745588b84a7 Mon Sep 17 00:00:00 2001 From: pguizeline Date: Thu, 21 Sep 2017 17:13:52 -0300 Subject: [PATCH 556/769] Add new GCP zones in South America (#680) --- algo | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/algo b/algo index 429a0e39..b74ca942 100755 --- a/algo +++ b/algo @@ -337,6 +337,9 @@ Name the vpn server: 31. Australia (Sydney A) 32. Australia (Sydney B) 33. Australia (Sydney C) + 34. South America (São Paulo A) + 35. South America (São Paulo B) + 36. South America (São Paulo C) Please choose the number of your zone. Press enter for default (#14) zone. [14]: " -r region region=${region:-14} @@ -375,6 +378,9 @@ Please choose the number of your zone. Press enter for default (#14) zone. 31) zone="australia-southeast1-a" ;; 32) zone="australia-southeast1-b" ;; 33) zone="australia-southeast1-c" ;; + 34) zone="southamerica-east1-a" ;; + 35) zone="southamerica-east1-b" ;; + 36) zone="southamerica-east1-c" ;; esac ROLES="gce vpn cloud" From e891d5c43b600be915bad0b94912913b366f8fc5 Mon Sep 17 00:00:00 2001 From: "Paul.W Harvey" Date: Sat, 30 Sep 2017 00:04:45 +1000 Subject: [PATCH 557/769] Generate stronger p12_export_password (#654) (#657) This buys us an extra 16bits of password guessing entropy by expanding the characterset from hex to [a-zA-Z0-9_@] --- playbooks/facts/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index 4887bb50..02d991ff 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -30,7 +30,7 @@ - name: Generate p12 export password local_action: module: shell - openssl rand -hex 4 + openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' become: no register: p12_export_password_generated when: p12_export_password is not defined From fee009688ecd2f3b02518f828ee472e15e56f26b Mon Sep 17 00:00:00 2001 From: Jacob Wilder Date: Sun, 1 Oct 2017 11:25:02 -0400 Subject: [PATCH 558/769] Change the second Canada Central to Canada East for Azure (#676) --- algo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algo b/algo index b74ca942..7403c3a7 100755 --- a/algo +++ b/algo @@ -127,7 +127,7 @@ Name the vpn server: 14. UK West 15. West US 16. Brazil South - 17. Canada Central + 17. Canada East 18. Central India 19. East Asia 20. Germany Central @@ -161,7 +161,7 @@ Enter the number of your desired region: 14) region="ukwest" ;; 15) region="westus" ;; 16) region="brazilsouth" ;; - 17) region="canadacentral" ;; + 17) region="canadaeast" ;; 18) region="centralindia" ;; 19) region="eastasia" ;; 20) region="germanycentral" ;; From 6b803e069f6622971896d8856abdd2b89678f254 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Sun, 1 Oct 2017 22:40:08 +0200 Subject: [PATCH 559/769] LibreSSL fix #625 (#685) --- roles/vpn/defaults/main.yml | 2 +- roles/vpn/tasks/openssl.yml | 43 +++++++++++++++--------------- roles/vpn/templates/openssl.cnf.j2 | 4 --- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 49f118d5..12f67887 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,5 +1,5 @@ --- - +openssl_bin: openssl strongswan_enabled_plugins: - aes - gcm diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index a1709bc0..b130b295 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -38,10 +38,10 @@ - name: Build the CA pair shell: > - openssl ecparam -name prime256v1 -out ecparams/prime256v1.pem && - openssl req -utf8 -new + {{ openssl_bin }} ecparam -name prime256v1 -out ecparams/prime256v1.pem && + {{ openssl_bin }} req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config openssl.cnf + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch @@ -50,8 +50,7 @@ args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" creates: "{{ IP_subject_alt_name }}_ca_generated" - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + executable: bash - name: Copy the CA certificate copy: @@ -67,52 +66,52 @@ - name: Build the server pair shell: > - openssl req -utf8 -new + {{ openssl_bin }} req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config openssl.cnf + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && - openssl ca -utf8 + {{ openssl_bin }} ca -utf8 -in reqs/{{ IP_subject_alt_name }}.req -out certs/{{ IP_subject_alt_name }}.crt - -config openssl.cnf -days 3650 -batch + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && touch certs/{{ IP_subject_alt_name }}_crt_generated args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" creates: certs/{{ IP_subject_alt_name }}_crt_generated - environment: - subjectAltName: "DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}" + executable: bash - name: Build the client's pair shell: > - openssl req -utf8 -new + {{ openssl_bin }} req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config openssl.cnf + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" -batch && - openssl ca -utf8 + {{ openssl_bin }} ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt - -config openssl.cnf -days 3650 -batch + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" creates: certs/{{ item }}_crt_generated - environment: - subjectAltName: "DNS:{{ item }}" + executable: bash with_items: "{{ users }}" - name: Build the client's p12 shell: > - openssl pkcs12 + {{ openssl_bin }} pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key -export @@ -122,6 +121,7 @@ -passout pass:"{{ easyrsa_p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" + executable: bash with_items: "{{ users }}" register: p12 @@ -145,16 +145,15 @@ - name: Revoke non-existing users shell: > - openssl ca -gencrl - -config openssl.cnf + {{ openssl_bin }} ca -gencrl + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt args: chdir: configs/{{ IP_subject_alt_name }}/pki/ creates: crl/{{ item }}.crt - environment: - subjectAltName: "DNS:{{ item }}" + executable: bash when: item not in users with_items: "{{ valid_certs.stdout_lines }}" diff --git a/roles/vpn/templates/openssl.cnf.j2 b/roles/vpn/templates/openssl.cnf.j2 index 9ec12b2d..d4cff0ca 100644 --- a/roles/vpn/templates/openssl.cnf.j2 +++ b/roles/vpn/templates/openssl.cnf.j2 @@ -110,7 +110,6 @@ authorityKeyIdentifier = keyid,issuer:always extendedKeyUsage = serverAuth,clientAuth,1.3.6.1.5.5.7.3.17 keyUsage = digitalSignature, keyEncipherment -subjectAltName = ${ENV::subjectAltName} # The Easy-RSA CA extensions [ easyrsa_ca ] @@ -138,6 +137,3 @@ keyUsage = cRLSign, keyCertSign # issuerAltName=issuer:copy authorityKeyIdentifier=keyid:always,issuer:always - - - From ee7264f26e07c090ce2264621ba8c8e68aea49ec Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 18 Oct 2017 22:15:39 +0200 Subject: [PATCH 560/769] Ask users to enter the p12 password manually (#697) --- roles/vpn/templates/client_windows.ps1.j2 | 2 +- roles/vpn/templates/mobileconfig.j2 | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 81baa6b2..b984ab10 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,6 +1,6 @@ function AddAlgoVPN { - certutil -f -p {{ easyrsa_p12_export_password }} -importpfx .\{{ item }}.p12 + certutil -f -importpfx .\{{ item }}.p12 Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -Force } diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 1a892d8f..ce51ea5a 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -146,8 +146,6 @@ IKEv2 - Password - {{ easyrsa_p12_export_password }} PayloadCertificateFileName {{ item.0 }}.p12 PayloadContent From 3c55cd15a4dea04d64016865dd75da5a879a1537 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 18 Oct 2017 22:23:57 +0200 Subject: [PATCH 561/769] GCE. replace underscores (#698) --- algo | 2 +- roles/cloud-gce/tasks/main.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/algo b/algo index 7403c3a7..392464e1 100755 --- a/algo +++ b/algo @@ -384,7 +384,7 @@ Please choose the number of your zone. Press enter for default (#14) zone. esac ROLES="gce vpn cloud" - EXTRA_VARS="credentials_file=$credentials_file server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1316" + EXTRA_VARS="credentials_file=$credentials_file gce_server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1316" } non_cloud () { diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 813aa578..f198b7ab 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -9,6 +9,7 @@ - set_fact: service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + server_name: "{{ gce_server_name | replace('_', '-') }}" - name: Network configured gce_net: From dc4dff040e229a22ffe14ea00a8b394dd6a12a1f Mon Sep 17 00:00:00 2001 From: Julie Bernosky Date: Thu, 19 Oct 2017 07:06:43 -0700 Subject: [PATCH 562/769] Add StrongSwan log level config option to ipsec.conf template (#700) --- config.cfg | 4 ++++ roles/vpn/templates/ipsec.conf.j2 | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 869c53c5..638ed14d 100644 --- a/config.cfg +++ b/config.cfg @@ -20,6 +20,10 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" +# StrongSwan log level +# https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration +strongswan_log_level: 2 + adblock_lists: - "http://winhelp2002.mvps.org/hosts.txt" - "https://adaway.org/hosts.txt" diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 313d6897..6c5a2d45 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -1,6 +1,6 @@ config setup uniqueids=never # allow multiple connections per user - charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" + charondebug="ike {{ strongswan_log_level }}, knl {{ strongswan_log_level }}, cfg {{ strongswan_log_level }}, net {{ strongswan_log_level }}, esp {{ strongswan_log_level }}, dmn {{ strongswan_log_level }}, mgr {{ strongswan_log_level }}" conn %default fragmentation=yes From 6572c2fb34c070caf44f86a9d784294ebdca022c Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Fri, 20 Oct 2017 22:16:28 -0400 Subject: [PATCH 563/769] Closes #699 --- config.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/config.cfg b/config.cfg index 638ed14d..10ea5cd2 100644 --- a/config.cfg +++ b/config.cfg @@ -14,7 +14,6 @@ users: easyrsa_reinit_existent: False vpn_network: 10.19.48.0/24 -# https://www.sixxs.net/tools/whois/?fd9d:bc11:4020::/48 vpn_network_ipv6: 'fd9d:bc11:4020::/48' server_name: "{{ ansible_ssh_host }}" From ea3766f02c4c8ad6f0eb192249e7fbc9cd067b70 Mon Sep 17 00:00:00 2001 From: Henrik Holmboe Date: Sat, 4 Nov 2017 07:15:40 +0100 Subject: [PATCH 564/769] Correct indentation for 'block' (#704) --- deploy.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deploy.yml b/deploy.yml index 91721c11..6caa70c8 100644 --- a/deploy.yml +++ b/deploy.yml @@ -6,15 +6,15 @@ pre_tasks: - block: - - name: Local pre-tasks - include: playbooks/local.yml - tags: [ 'always' ] + - name: Local pre-tasks + include: playbooks/local.yml + tags: [ 'always' ] - - name: Local pre-tasks - include: playbooks/local_ssh.yml - become: false - when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" - tags: [ 'local' ] + - name: Local pre-tasks + include: playbooks/local_ssh.yml + become: false + when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" + tags: [ 'local' ] rescue: - debug: var=fail_hint tags: always From 185c0f51d7fcf00784582db4d18d9b08750e0665 Mon Sep 17 00:00:00 2001 From: Jurgen Verhasselt Date: Sat, 4 Nov 2017 07:16:29 +0100 Subject: [PATCH 565/769] correct configs_prefix vars in client tasks (#712) --- roles/client/tasks/systems/CentOS.yml | 2 +- roles/client/tasks/systems/Debian.yml | 2 +- roles/client/tasks/systems/Fedora.yml | 2 +- roles/client/tasks/systems/Ubuntu.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/client/tasks/systems/CentOS.yml b/roles/client/tasks/systems/CentOS.yml index 60df753f..aeb495e5 100644 --- a/roles/client/tasks/systems/CentOS.yml +++ b/roles/client/tasks/systems/CentOS.yml @@ -3,4 +3,4 @@ - set_fact: prerequisites: - epel-release - configs_prefix: /etc/strongswan/ + configs_prefix: /etc/strongswan diff --git a/roles/client/tasks/systems/Debian.yml b/roles/client/tasks/systems/Debian.yml index 9e5461d2..2566c076 100644 --- a/roles/client/tasks/systems/Debian.yml +++ b/roles/client/tasks/systems/Debian.yml @@ -2,4 +2,4 @@ - set_fact: prerequisites: [] - configs_prefix: /etc/ + configs_prefix: /etc diff --git a/roles/client/tasks/systems/Fedora.yml b/roles/client/tasks/systems/Fedora.yml index ec920927..6bc13ef4 100644 --- a/roles/client/tasks/systems/Fedora.yml +++ b/roles/client/tasks/systems/Fedora.yml @@ -3,4 +3,4 @@ - set_fact: prerequisites: - libselinux-python - configs_prefix: /etc/strongswan/ + configs_prefix: /etc/strongswan diff --git a/roles/client/tasks/systems/Ubuntu.yml b/roles/client/tasks/systems/Ubuntu.yml index 9e5461d2..2566c076 100644 --- a/roles/client/tasks/systems/Ubuntu.yml +++ b/roles/client/tasks/systems/Ubuntu.yml @@ -2,4 +2,4 @@ - set_fact: prerequisites: [] - configs_prefix: /etc/ + configs_prefix: /etc From b64f682baea589266ac6995b79d490ea3d6a35f6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 8 Nov 2017 18:22:58 +0300 Subject: [PATCH 566/769] remove the dead code. Fixes #671 --- roles/security/tasks/main.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml index 4289ad1f..2f279122 100644 --- a/roles/security/tasks/main.yml +++ b/roles/security/tasks/main.yml @@ -56,13 +56,6 @@ group: root mode: 0750 - - name: Collect Use of privileged commands - shell: > - /usr/bin/find {/usr/local/sbin,/usr/local/bin,/sbin,/bin,/usr/sbin,/usr/bin} -xdev \( -perm -4000 -o -perm -2000 \) -type f | awk '{print "-a always,exit -F path=" $1 " -F perm=x -F auid>=500 -F auid!=4294967295 -k privileged" }' - args: - executable: /bin/bash - register: privileged_programs - # Core dumps - name: Restrict core dumps (with PAM) From f18c1a0d67ba024eff607f13c2bccfe07cfc602b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 13 Nov 2017 00:09:57 +0200 Subject: [PATCH 567/769] Certificate revocation fix (#719) --- roles/vpn/handlers/main.yml | 2 +- roles/vpn/tasks/openssl.yml | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/roles/vpn/handlers/main.yml b/roles/vpn/handlers/main.yml index 9b481d43..8ce3163a 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/vpn/handlers/main.yml @@ -14,4 +14,4 @@ service: name=netfilter-persistent state=restarted - name: rereadcrls - shell: ipsec rereadcrls + shell: ipsec rereadcrls; ipsec purgecrls diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index b130b295..1c3e61bf 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -150,6 +150,7 @@ -passin pass:"{{ easyrsa_CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt + register: gencrl args: chdir: configs/{{ IP_subject_alt_name }}/pki/ creates: crl/{{ item }}.crt @@ -157,14 +158,27 @@ when: item not in users with_items: "{{ valid_certs.stdout_lines }}" + - name: Genereate new CRL file + shell: > + {{ openssl_bin }} ca -gencrl + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }}")) + -passin pass:"{{ easyrsa_CA_password }}" + -out crl/algo.root.pem + when: + - gencrl is defined + - gencrl.changed + args: + chdir: configs/{{ IP_subject_alt_name }}/pki/ + executable: bash delegate_to: localhost become: no -- name: Copy the revoked certificates to the vpn server +- name: Copy the CRL to the vpn server copy: - src: configs/{{ IP_subject_alt_name }}/pki/crl/{{ item }}.crt - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/{{ item }}.crt" - when: item not in users - with_items: "{{ valid_certs.stdout_lines }}" + src: configs/{{ IP_subject_alt_name }}/pki/crl/algo.root.pem + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem" + when: + - gencrl is defined + - gencrl.changed notify: - rereadcrls From d08e5259066efa976ce084ed652a13fafef49a96 Mon Sep 17 00:00:00 2001 From: Jurgen Verhasselt Date: Sun, 12 Nov 2017 23:10:19 +0100 Subject: [PATCH 568/769] Docs to deploy from, and setup client on, Fedora Workstation (#711) * docs/client-linux.md housekeeping * add fedora-workstation instructions to client-linx.md * add deploy-from-fedora-workstation doc * change client-linux.md to internal link * add deploy-from-fedora-workstation links * correct markup * correct typo --- README.md | 1 + docs/client-linux.md | 67 ++++++++++++- docs/deploy-from-fedora-workstation.md | 126 +++++++++++++++++++++++++ docs/index.md | 1 + 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 docs/deploy-from-fedora-workstation.md diff --git a/README.md b/README.md index 1a6c2298..55b1bd6f 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ After this process completes, the Algo VPN server will contains only the users l * Setup instructions - Documentation for available [Ansible roles](docs/setup-roles.md) + - Deploy from [Fedora Workstation (26)](docs/deploy-from-fedora-workstation.md) - Deploy from [RedHat/CentOS 6.x](docs/deploy-from-redhat-centos6.md) - Deploy from [Windows](docs/deploy-from-windows.md) - Deploy from [Ansible](docs/deploy-from-ansible.md) directly diff --git a/docs/client-linux.md b/docs/client-linux.md index 89e17e58..a5155501 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -1,19 +1,78 @@ # Linux client setup +## Provision client config + After you deploy a server, you can use an included Ansible script to provision Linux clients too! Debian, Ubuntu, CentOS, and Fedora are supported. The playbook is `deploy_client.yml`. -### Required variables: +### Required variables * `client_ip` - The IP address of your client machine (You can use `localhost` in order to deploy locally) * `vpn_user` - The username. (Ensure that you have valid certificates and keys in the `configs/SERVER_ip/pki/` directory) * `ssh_user` - The username that we need to use in order to connect to the client machine via SSH (ignore if you are deploying locally) * `server_ip` - The vpn server ip address -### Example: +### Example ```shell ansible-playbook deploy_client.yml -e 'client_ip=client.com vpn_user=jack server_ip=vpn-server.com ssh_user=root' ``` -### Additional options: -If the user requires sudo password use the following argument: `--ask-become-pass` +### Additional options + +If the user requires sudo password use the following argument: `--ask-become-pass`. + +## OS Specific instructions + +Some Linux clients may require more specific and details instructions to configure a connection to the deployed Algo VPN, these are documented here. + +### Fedora Workstation + +#### (Gnome) Network Manager install + +We'll use the [rsclarke/NetworkManager-strongswan](https://copr.fedorainfracloud.org/coprs/rsclarke/NetworkManager-strongswan/) Copr repo (see [this comment](https://github.com/trailofbits/algo/issues/263#issuecomment-327820191)), this will make the `IKE` and `ESP` fields available in the Gnome Network Manager. Note that at time of writing the non-Copr repo will result in connection failures. Also note that the Copr repo *instructions are not filled in by author. Author knows what to do. Everybody else should avoid this repo*. So unless you are comfortable with using this repo, you'll want to hold out untill the patches applied in the Copr repo make it into stable. + +First remove the stable `NetworkManager-strongswan` package, ensure you have backups in place and / or take note of config backups taken during the removal of the package. + +```` +dnf remove NetworkManager-strongswan +```` + +Next, enable the Copr repo and install it along with the `NetworkManager-strongswan-gnome` package: + +```` +dnf copr enable -y rsclarke/NetworkManager-strongswan +dnf install NetworkManager-strongswan NetworkManager-strongswan-gnome +```` + +Reboot your machine: + +```` +reboot now +```` + +#### (Gnome) Network Manager configuration + +In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the user we created is `user-name`. + +* Go to *Settings* > *Network* +* Add a new Network (`+` bottom left of the window) +* Select *IPsec/IKEv2 (strongswan)* +* Fill out the options: + * Name: your choice, e.g.: *ikev2-1.2.3.4* + * Gateway: + * Address: IP of the Algo VPN server, e.g: `1.2.3.4` + * Certificate: `cacert.pem` found at `/path/to/algo/1.2.3.4/cacert.pem` + * Client: + * Authentication: *Certificate/Private key* + * Certificate: `user-name.crt` found at `/path/to/algo/1.2.3.4/pki/certs/user-name.crt` + * Private key: `user-name.key` found at `/path/to/algo/1.2.3.4/pki/private/user-name.key` + * Options: + * Check *Request an inner IP address*, connection will fail without this option + * Optionally check *Enforce UDP encapsulation* + * Optionally check *Use IP compression* + * For the later 2 options, hover to option in the settings to see a description + * Cipher proposal: + * Check *Enable custom proposals* + * IKE: `aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256` + * ESP: `aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256` +* Apply and turn the connection on, you should now be connected \ No newline at end of file diff --git a/docs/deploy-from-fedora-workstation.md b/docs/deploy-from-fedora-workstation.md new file mode 100644 index 00000000..319d74cf --- /dev/null +++ b/docs/deploy-from-fedora-workstation.md @@ -0,0 +1,126 @@ +# Deploy from Fedora Workstation + +These docs were written based on experience on Fedora Workstation 26. + +## Prerequisites + +### DNF counterparts of apt packages + +The following table lists `apt` packages with their `dnf` counterpart. This is purely informative. +Using `python2-*` in favour of `python3-*` as per [declared dependency](https://github.com/trailofbits/algo#deploy-the-algo-server). + +| `apt` | `dnf` | +| ----- | ----- | +| `build-essential` | `make automake gcc gcc-c++ kernel-devel` | +| `libssl-dev` | `openssl-devel` | +| `libffi-dev` | `libffi-devel` | +| `python-dev` | `python-devel` | +| `python-pip` | `python2-pip` | +| `python-setuptools` | `python2-setuptools` | +| `python-virtualenv` | `python2-virtualenv` | + +### Install requirements + +First, let's make sure our system is up-to-date: + +```` +dnf upgrade +```` + +Next, install the required packages: + +```` +dnf install -y \ + ansible \ + automake \ + gcc \ + gcc-c++ \ + kernel-devel \ + openssl-devel \ + libffi-devel \ + libselinux-python \ + python-devel \ + python2-pip \ + python2-setuptools \ + python2-virtualenv \ + make +```` + +## Get Algo + + +[Download](https://github.com/trailofbits/algo/archive/master.zip) or clone: + +```` +git clone git@github.com:trailofbits/algo.git +cd algo +```` + +If you downloaded Algo, unzip to your prefered location and `cd` into it. +We'll assume from this point forward that our working directory is the `algo` root directory. + + +## Prepare algo + +Some steps are needed before we can deploy our Algo VPN server. + +### Check `pip` + +Run `pip -v` and check the python version it is using: + +```` +$ pip -V +pip 9.0.1 from /usr/lib/python2.7/site-packages (python 2.7) +```` + +`python 2.7` is what we're looking for. + +### `pip` upgrade and installs + +```` +# Upgrade pip itself +pip -q install --upgrade pip +# python-devel needed to prevent setup.py crash +pip -q install pycrypto +# pycrypto 2.7.1 needed for latest security patch +# This may need to run with sudo to complete without permission violations +pip -q install setuptools --upgrade +# virtualenv to make installing dependencies easier +pip -q install virtualenv +```` + +### Setup virtualenv and install requirements + +```` +virtualenv --system-site-packages env +source env/bin/activate +pip -q install --user -r requirements.txt +```` + +## Configure + +Edit the userlist and any other settings you desire in `config.cfg` using your prefered editor. + +## Deploy + +We can now deploy our server by running: + +```` +./algo +```` + +Ensure to allow Windows / Linux clients when going through the config options. +Note the IP and password of the newly created Alfo VPN server and store it safely. + +If you want to setup client config on your Fedora Workstation, refer to [the Linux Client docs](client-linux.md). + +## Notes on SELinux + +If you have SELinux enabled, you'll need to set appropriate file contexts: + +```` +semanage fcontext -a -t ipsec_key_file_t "$(pwd)(/.*)?" +restorecon -R -v $(pwd) +```` + +See [this comment](https://github.com/trailofbits/algo/issues/263#issuecomment-328053950). diff --git a/docs/index.md b/docs/index.md index 7275901f..b9b94bb6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,7 @@ * Setup instructions - Documentation for available [Ansible roles](setup-roles.md) + - Deploy from [Fedora Workstation (26)](deploy-from-fedora-workstation.md) - Deploy from [RedHat/CentOS 6.x](deploy-from-redhat-centos6.md) - Deploy from [Windows](deploy-from-windows.md) - Deploy from [Ansible](deploy-from-ansible.md) directly From e01521bbf493d04dd5ffc7942b558d50674b414f Mon Sep 17 00:00:00 2001 From: Allan Date: Mon, 13 Nov 2017 01:49:58 +0200 Subject: [PATCH 569/769] Update to deploy-to-ubuntu.md (#628) * Update to deploy-to-ubuntu.md A fresh install (Off CD / ISO) doesn't include python-pip or python virtualenv module. The fixes above take care of the additional requirements, as well as updating pip. * Update deploy-to-ubuntu.md Fix Typo --- docs/deploy-to-ubuntu.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 929b7192..62e58f94 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -8,9 +8,13 @@ tl;dr: ```shell sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible +sudo apt-get update && sudo apt-get install ansible python-pip +pip install virtualenv +pip install --upgrade pip git clone https://github.com/trailofbits/algo -cd algo && ./algo +cd algo +python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt +./algo ``` **Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). From bd4ea1235fd3224046a59fed7185ca2a93e34e22 Mon Sep 17 00:00:00 2001 From: Brian Harrington Date: Tue, 21 Nov 2017 13:49:54 +0800 Subject: [PATCH 570/769] GCE correct variable key (#734) `server_name` should be `gce_server_name` for Google Compute Engine --- docs/deploy-from-ansible.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 5c92a32b..646d838a 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -180,7 +180,7 @@ Additional tags: Required variables: - credentials_file -- server_name +- gce_server_name - ssh_public_key - zone From 07a1c70bf4ffc666fb6eaba54031b081b5542b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20Elizeche=20Land=C3=B3?= Date: Tue, 21 Nov 2017 02:50:05 -0300 Subject: [PATCH 571/769] Update adblock.sh for systemd to fix issue #735 (#736) * Update script to restart the dnsmasq service using systemctl(systemd) command instead of service(Upstart) * Use instead of legacy REF: https://github.com/koalaman/shellcheck/wiki/SC2006 * Replace non-standard egrep(deprecated) for grep -E. REF: https://github.com/koalaman/shellcheck/wiki/SC2196 --- roles/dns_adblocking/templates/adblock.sh.j2 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/roles/dns_adblocking/templates/adblock.sh.j2 b/roles/dns_adblocking/templates/adblock.sh.j2 index def11d3a..08af3623 100644 --- a/roles/dns_adblocking/templates/adblock.sh.j2 +++ b/roles/dns_adblocking/templates/adblock.sh.j2 @@ -1,8 +1,8 @@ #!/bin/sh # Block ads, malware, etc.. -TEMP=`mktemp` -TEMP_SORTED=`mktemp` +TEMP="$(mktemp)" +TEMP_SORTED="$(mktemp)" DNSMASQ_WHITELIST="/var/lib/dnsmasq/white.list" DNSMASQ_BLACKLIST="/var/lib/dnsmasq/black.list" DNSMASQ_BLOCKHOSTS="{{ config_prefix|default('/') }}etc/dnsmasq.d/block.hosts.conf" @@ -33,11 +33,13 @@ then #Filter the blacklist, suppressing whitelist matches # This is relatively slow =-( echo 'Filtering white list...' - egrep -v "^[[:space:]]*$" $DNSMASQ_WHITELIST | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS + grep -v -E "^[[:space:]]*$" $DNSMASQ_WHITELIST | awk '/^[^#]/ {sub(/\r$/,"");print $1}' | grep -vf - "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS else cat "$TEMP_SORTED" > $DNSMASQ_BLOCKHOSTS fi -service dnsmasq restart +echo 'Restarting dnsmasq service...' +#Restart the dnsmasq service +systemctl restart dnsmasq.service exit 0 From a844870b7a2d68fc26ed8fbc277160f01ba50611 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 22 Nov 2017 17:15:43 +0300 Subject: [PATCH 572/769] Sendmail should not be installed (#738) --- roles/common/tasks/ubuntu.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index b512af61..27d1112e 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -78,7 +78,6 @@ - apparmor-utils - uuid-runtime - coreutils - - sendmail - iptables-persistent - cgroup-tools - openssl From d9b1b22fac1206b6b1037564ebd6baf9f6fdef68 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 19 Jan 2018 17:19:32 +0300 Subject: [PATCH 573/769] Place the module digital_ocean_tag with the fix (#782) zesty is no longer available disable ubuntu 17 at all --- .travis.yml | 2 +- library/digital_ocean_tag.py | 258 +++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 library/digital_ocean_tag.py diff --git a/.travis.yml b/.travis.yml index f4780b48..c0b56d1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ before_cache: env: - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial - - LXC_NAME=ubuntu1704 LXC_DISTRO=ubuntu LXC_RELEASE=zesty + # - LXC_NAME=ubuntu1710 LXC_DISTRO=ubuntu LXC_RELEASE=artful install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." diff --git a/library/digital_ocean_tag.py b/library/digital_ocean_tag.py new file mode 100644 index 00000000..b80d18b5 --- /dev/null +++ b/library/digital_ocean_tag.py @@ -0,0 +1,258 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . +DOCUMENTATION = ''' +--- +module: digital_ocean_tag +short_description: Create and remove tag(s) to DigitalOcean resource. +description: + - Create and remove tag(s) to DigitalOcean resource. +version_added: "2.2" +options: + name: + description: + - The name of the tag. The supported characters for names include + alphanumeric characters, dashes, and underscores. + required: true + resource_id: + description: + - The ID of the resource to operate on. + resource_type: + description: + - The type of resource to operate on. Currently only tagging of + droplets is supported. + default: droplet + choices: ['droplet'] + state: + description: + - Whether the tag should be present or absent on the resource. + default: present + choices: ['present', 'absent'] + api_token: + description: + - DigitalOcean api token. + +notes: + - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. + They both refer to the v2 token. + - As of Ansible 2.0, Version 2 of the DigitalOcean API is used. + +requirements: + - "python >= 2.6" +''' + + +EXAMPLES = ''' +- name: create a tag + digital_ocean_tag: + name: production + state: present + +- name: tag a resource; creating the tag if it does not exists + digital_ocean_tag: + name: "{{ item }}" + resource_id: YYY + state: present + with_items: + - staging + - dbserver + +- name: untag a resource + digital_ocean_tag: + name: staging + resource_id: YYY + state: absent + +# Deleting a tag also untags all the resources that have previously been +# tagged with it +- name: remove a tag + digital_ocean_tag: + name: dbserver + state: absent +''' + + +RETURN = ''' +data: + description: a DigitalOcean Tag resource + returned: success and no resource constraint + type: dict + sample: { + "tag": { + "name": "awesome", + "resources": { + "droplets": { + "count": 0, + "last_tagged": null + } + } + } + } +''' + +import json +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url + + +class Response(object): + + def __init__(self, resp, info): + self.body = None + if resp: + self.body = resp.read() + self.info = info + + @property + def json(self): + if not self.body: + if "body" in self.info: + return json.loads(self.info["body"]) + return None + try: + return json.loads(self.body) + except ValueError: + return None + + @property + def status_code(self): + return self.info["status"] + + +class Rest(object): + + def __init__(self, module, headers): + self.module = module + self.headers = headers + self.baseurl = 'https://api.digitalocean.com/v2' + + def _url_builder(self, path): + if path[0] == '/': + path = path[1:] + return '%s/%s' % (self.baseurl, path) + + def send(self, method, path, data=None, headers=None): + url = self._url_builder(path) + data = self.module.jsonify(data) + + resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method) + + return Response(resp, info) + + def get(self, path, data=None, headers=None): + return self.send('GET', path, data, headers) + + def put(self, path, data=None, headers=None): + return self.send('PUT', path, data, headers) + + def post(self, path, data=None, headers=None): + return self.send('POST', path, data, headers) + + def delete(self, path, data=None, headers=None): + return self.send('DELETE', path, data, headers) + + +def core(module): + try: + api_token = module.params['api_token'] or \ + os.environ['DO_API_TOKEN'] or os.environ['DO_API_KEY'] + except KeyError as e: + module.fail_json(msg='Unable to load %s' % e.message) + + state = module.params['state'] + name = module.params['name'] + resource_id = module.params['resource_id'] + resource_type = module.params['resource_type'] + + rest = Rest(module, {'Authorization': 'Bearer {}'.format(api_token), + 'Content-type': 'application/json'}) + + if state in ('present'): + if name is None: + module.fail_json(msg='parameter `name` is missing') + + # Ensure Tag exists + response = rest.post("tags", data={'name': name}) + status_code = response.status_code + json = response.json + if status_code == 201: + changed = True + elif status_code == 422: + changed = False + else: + module.exit_json(changed=False, data=json) + + if resource_id is None: + # No resource defined, we're done. + if json is None: + module.exit_json(changed=changed, data=json) + else: + module.exit_json(changed=changed, data=json) + else: + # Tag a resource + url = "tags/{}/resources".format(name) + payload = { + 'resources': [{ + 'resource_id': resource_id, + 'resource_type': resource_type}]} + response = rest.post(url, data=payload) + if response.status_code == 204: + module.exit_json(changed=True) + else: + module.fail_json(msg="error tagging resource '{}': {}".format( + resource_id, response.json["message"])) + + elif state in ('absent'): + if name is None: + module.fail_json(msg='parameter `name` is missing') + + if resource_id: + url = "tags/{}/resources".format(name) + payload = { + 'resources': [{ + 'resource_id': resource_id, + 'resource_type': resource_type}]} + response = rest.delete(url, data=payload) + else: + url = "tags/{}".format(name) + response = rest.delete(url) + if response.status_code == 204: + module.exit_json(changed=True) + else: + module.exit_json(changed=False, data=response.json) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', required=True), + resource_id=dict(aliases=['droplet_id'], type='str'), + resource_type=dict(choices=['droplet'], default='droplet'), + state=dict(choices=['present', 'absent'], default='present'), + api_token=dict(aliases=['API_TOKEN'], no_log=True), + ) + ) + + try: + core(module) + except Exception as e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + main() From 7eb4fc5f22dc1627f36cc9aebcaa9f7ae2a9dc95 Mon Sep 17 00:00:00 2001 From: Douglas Gastonguay-Goddard Date: Fri, 19 Jan 2018 20:06:15 -0500 Subject: [PATCH 574/769] DigitalOcean - Add cleanup step for SSH key (#784) * Add cleanup step for SSH key. * Two space tabs are hard to see. --- roles/cloud-digitalocean/tasks/main.yml | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 66308423..3fec1e4c 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -101,6 +101,33 @@ line: "{{ item.networks.v4[0].ip_address }}" with_items: - "{{ do_droplets.json.droplets }}" + + - block: + - name: "Delete the new Algo SSH key" + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 + + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + + - fail: + msg: "Please, ensure that your API token is not read-only." rescue: - debug: var=fail_hint tags: always From 054dc0afcd801022ea875a2f290e8b082e697f5b Mon Sep 17 00:00:00 2001 From: Achim Staebler <4sm0d3us@gmail.com> Date: Wed, 24 Jan 2018 18:03:47 +0100 Subject: [PATCH 575/769] Instructions for Ubuntu needed compiler install (#791) build-essential and python-dev are required when compiling pycrypt. Added the necessary packages to the apt-get install line. --- docs/deploy-to-ubuntu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 62e58f94..5516611b 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -8,7 +8,7 @@ tl;dr: ```shell sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible python-pip +sudo apt-get update && sudo apt-get install ansible python-pip build-essential python-dev pip install virtualenv pip install --upgrade pip git clone https://github.com/trailofbits/algo From 5eed1bbba483494559a6234645ea833914e4b3ca Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Sat, 27 Jan 2018 14:01:12 -0600 Subject: [PATCH 576/769] Use dns_servers in dnsmasq.conf (#794) --- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 3b8c4c5c..3424d544 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -88,8 +88,9 @@ # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 -server=8.8.8.8 -server=8.8.4.4 +{% for host in dns_servers.ipv4 %} +server={{ host }} +{% endfor %} # and this sets the source (ie local) address used to talk to # 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that From d8f0393dd8d9a88fe72dec47f0290e9990e9e125 Mon Sep 17 00:00:00 2001 From: Dan Ackerson Date: Sat, 27 Jan 2018 21:02:00 +0100 Subject: [PATCH 577/769] minimum DigitalOcean $5 type now 's-1vcpu-1gb' (#785) https://www.digitalocean.com/pricing/ --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 10ea5cd2..108b8cf6 100644 --- a/config.cfg +++ b/config.cfg @@ -76,7 +76,7 @@ cloud_providers: sku: '16.04-LTS' # 16.04-LTS / 17.04 version: latest digitalocean: - size: 512mb + size: s-1vcpu-1gb image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-04-x64 ec2: size: t2.micro From 4da752b60390b09038fb816a372a9b2ffdc30b0c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 24 Feb 2018 16:17:34 +0300 Subject: [PATCH 578/769] Ubuntu 17.10 support (#811) --- .travis.yml | 2 +- config.cfg | 2 +- roles/common/tasks/ubuntu.yml | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c0b56d1f..c751a6eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ before_cache: env: - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial - # - LXC_NAME=ubuntu1710 LXC_DISTRO=ubuntu LXC_RELEASE=artful + - LXC_NAME=ubuntu1710 LXC_DISTRO=ubuntu LXC_RELEASE=artful install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." diff --git a/config.cfg b/config.cfg index 108b8cf6..40382e61 100644 --- a/config.cfg +++ b/config.cfg @@ -77,7 +77,7 @@ cloud_providers: version: latest digitalocean: size: s-1vcpu-1gb - image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-04-x64 + image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-10-x64 ec2: size: t2.micro image: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 27d1112e..4c5705e4 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -44,6 +44,23 @@ tags: - cloud +- name: Install system specific tools + package: name="{{ item }}" state=present + with_items: + - ifupdown + tags: + - always + +- name: Ensure the interfaces directory exists + file: + path: /etc/network/interfaces.d/ + state: directory + mode: 0755 + owner: root + group: root + tags: + - always + - name: Loopback for services configured template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg notify: From 02427910de8f6765c91ac5084e8712ded7dbe78c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 2 Mar 2018 15:55:54 +0300 Subject: [PATCH 579/769] Ansible 2.4, Lightsail, Scaleway, DreamCompute (OpenStack) integration (#804) * Move to ansible-2.4.3 * Add Lightsail support #623 * Fixing the EC2 deployment * Scaleway integration #623 * OpenStack cloud provider (DreamCompute optimised) #623 * Remove the security role * Enable unattended-upgrades for clouds * New requirements to make Azure and GCE work --- .travis.yml | 2 +- algo | 137 ++++- config.cfg | 10 + deploy.yml | 14 +- library/digital_ocean_tag.py | 197 +++---- library/lightsail.py | 551 ++++++++++++++++++ playbooks/common.yml | 6 +- playbooks/freebsd.yml | 2 +- playbooks/post.yml | 2 +- requirements.txt | 7 +- roles/client/tasks/main.yml | 2 +- roles/client/tasks/systems/main.yml | 8 +- roles/cloud-ec2/tasks/main.yml | 6 +- roles/cloud-lightsail/tasks/main.yml | 52 ++ roles/cloud-openstack/tasks/main.yml | 87 +++ roles/cloud-scaleway/tasks/main.yml | 128 ++++ roles/common/tasks/main.yml | 4 +- roles/common/tasks/ubuntu.yml | 70 ++- roles/common/tasks/unattended-upgrades.yml | 21 + .../templates/10periodic.j2 | 2 +- .../templates/50unattended-upgrades.j2 | 4 +- roles/dns_adblocking/tasks/main.yml | 4 +- roles/security/handlers/main.yml | 5 - roles/security/meta/main.yml | 4 - roles/security/tasks/main.yml | 161 ----- roles/security/templates/sshd_config.j2 | 51 -- roles/vpn/tasks/main.yml | 12 +- roles/vpn/tasks/ubuntu.yml | 2 +- users.yml | 2 +- 29 files changed, 1123 insertions(+), 430 deletions(-) create mode 100644 library/lightsail.py create mode 100644 roles/cloud-lightsail/tasks/main.yml create mode 100644 roles/cloud-openstack/tasks/main.yml create mode 100644 roles/cloud-scaleway/tasks/main.yml create mode 100644 roles/common/tasks/unattended-upgrades.yml rename roles/{security => common}/templates/10periodic.j2 (76%) rename roles/{security => common}/templates/50unattended-upgrades.j2 (97%) delete mode 100644 roles/security/handlers/main.yml delete mode 100644 roles/security/meta/main.yml delete mode 100644 roles/security/tasks/main.yml delete mode 100644 roles/security/templates/sshd_config.j2 diff --git a/.travis.yml b/.travis.yml index c751a6eb..ae0adc43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ script: # - shellcheck algo # - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check - - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" + - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" after_script: - ./tests/update-users.sh diff --git a/algo b/algo index 392464e1..dca852c3 100755 --- a/algo +++ b/algo @@ -48,12 +48,6 @@ Do you want each user to have their own account for SSH tunneling? ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi -read -p " -Do you want to apply operating system security enhancements on the server? (warning: replaces your sshd_config) -[y/N]: " -r security_enabled -security_enabled=${security_enabled:-n} -if [[ "$security_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" security"; fi - read -p " Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) [y/N]: " -r Win10_Enabled @@ -290,6 +284,115 @@ Enter the number of your desired region: EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" } +lightsail () { +read -p " +Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). +$ADDITIONAL_PROMPT +[AKIA...]: " -rs aws_access_key + +read -p " + +Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +$ADDITIONAL_PROMPT +[ABCD...]: " -rs aws_secret_key + +read -p " + +Name the vpn server: +[algo.local]: " -r algo_server_name + algo_server_name=${algo_server_name:-algo.local} + + read -p " + + What region should the server be located in? + 1. us-east-1 US East (N. Virginia) + 2. us-east-2 US East (Ohio) + 3. us-west-1 US West (N. California) + 4. us-west-2 US West (Oregon) + 5. ap-south-1 Asia Pacific (Mumbai) + 6. ap-northeast-2 Asia Pacific (Seoul) + 7. ap-southeast-1 Asia Pacific (Singapore) + 8. ap-southeast-2 Asia Pacific (Sydney) + 9. ap-northeast-1 Asia Pacific (Tokyo) + 10. eu-central-1 EU (Frankfurt) + 11. eu-west-1 EU (Ireland) + 12. eu-west-2 EU (London) +Enter the number of your desired region: +[1]: " -r algo_region +algo_region=${algo_region:-1} + + case "$algo_region" in + 1) region="us-east-1" ;; + 2) region="us-east-2" ;; + 3) region="us-west-1" ;; + 4) region="us-west-2" ;; + 5) region="ap-south-1" ;; + 6) region="ap-northeast-2" ;; + 7) region="ap-southeast-1" ;; + 8) region="ap-southeast-2" ;; + 9) region="ap-northeast-1" ;; + 10) region="eu-central-1" ;; + 11) region="eu-west-1" ;; + 12) region="eu-west-2";; + esac + + ROLES="lightsail vpn cloud" + EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key algo_server_name=$algo_server_name region=$region" +} + +scaleway () { +read -p " +Enter your auth token (https://www.scaleway.com/docs/generate-an-api-token/) +$ADDITIONAL_PROMPT +[...]: " -rs scaleway_auth_token + +read -p " + +Enter your organization name (https://cloud.scaleway.com/#/billing) +$ADDITIONAL_PROMPT +[...]: " -rs scaleway_organization + +read -p " + +Name the vpn server: +[algo.local]: " -r algo_server_name + algo_server_name=${algo_server_name:-algo.local} + + read -p " + + What region should the server be located in? + 1. par1 Paris + 2. ams1 Amsterdam +Enter the number of your desired region: +[1]: " -r algo_region +algo_region=${algo_region:-1} + + case "$algo_region" in + 1) region="par1" ;; + 2) region="ams1" ;; + esac + + ROLES="scaleway vpn cloud" + EXTRA_VARS="scaleway_auth_token=$scaleway_auth_token scaleway_organization=\"$scaleway_organization\" algo_server_name=$algo_server_name algo_region=$region" +} + +openstack () { +read -p " +Enter the local path to your credentials OpenStack RC file (Can be donloaded from the OpenStack dashboard->Compute->API Access) +[...]: " -r os_rc + +read -p " + +Name the vpn server: +[algo.local]: " -r algo_server_name + algo_server_name=${algo_server_name:-algo.local} + + ROLES="openstack vpn cloud" + EXTRA_VARS="algo_server_name=$algo_server_name" + source $os_rc +} + gce () { read -p " Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): @@ -433,10 +536,13 @@ algo_provisioning () { echo -n " What provider would you like to use? 1. DigitalOcean - 2. Amazon EC2 - 3. Microsoft Azure - 4. Google Compute Engine - 5. Install to existing Ubuntu 16.04 server + 2. Amazon Lightsail + 3. Amazon EC2 + 4. Microsoft Azure + 5. Google Compute Engine + 6. Scaleway + 7. OpenStack (DreamCompute optimised) + 8. Install to existing Ubuntu 16.04 server Enter the number of your desired provider : " @@ -445,10 +551,13 @@ Enter the number of your desired provider case "$N" in 1) digitalocean; ;; - 2) ec2; ;; - 3) azure; ;; - 4) gce; ;; - 5) non_cloud; ;; + 2) lightsail; ;; + 3) ec2; ;; + 4) azure; ;; + 5) gce; ;; + 6) scaleway; ;; + 7) openstack; ;; + 8) non_cloud; ;; *) exit 1 ;; esac diff --git a/config.cfg b/config.cfg index 40382e61..d5cc0a55 100644 --- a/config.cfg +++ b/config.cfg @@ -86,6 +86,16 @@ cloud_providers: gce: size: f1-micro image: ubuntu-1604 # ubuntu-1604 / ubuntu-1704 + lightsail: + size: nano_1_0 + image: ubuntu_16_04 + scaleway: + size: VC1S + image: Ubuntu Xenial + arch: x86_64 + openstack: + flavor_ram: ">=512" + image: Ubuntu-16.04 local: fail_hint: diff --git a/deploy.yml b/deploy.yml index 6caa70c8..fa5212ec 100644 --- a/deploy.yml +++ b/deploy.yml @@ -7,11 +7,11 @@ pre_tasks: - block: - name: Local pre-tasks - include: playbooks/local.yml + include_tasks: playbooks/local.yml tags: [ 'always' ] - name: Local pre-tasks - include: playbooks/local_ssh.yml + include_tasks: playbooks/local_ssh.yml become: false when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" tags: [ 'local' ] @@ -26,12 +26,15 @@ - { role: cloud-ec2, tags: ['ec2'] } - { role: cloud-gce, tags: ['gce'] } - { role: cloud-azure, tags: ['azure'] } + - { role: cloud-lightsail, tags: ['lightsail'] } + - { role: cloud-scaleway, tags: ['scaleway'] } + - { role: cloud-openstack, tags: ['openstack'] } - { role: local, tags: ['local'] } post_tasks: - block: - name: Local post-tasks - include: playbooks/post.yml + include_tasks: playbooks/post.yml become: false tags: [ 'cloud' ] rescue: @@ -51,8 +54,8 @@ pre_tasks: - block: - name: Common pre-tasks - include: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'local', 'pre' ] + include_tasks: playbooks/common.yml + tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'lightsail', 'scaleway', 'openstack', 'local', 'pre' ] rescue: - debug: var=fail_hint tags: always @@ -60,7 +63,6 @@ tags: always roles: - - { role: security, tags: [ 'security' ] } - { role: dns_adblocking, tags: ['dns', 'adblock' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - { role: vpn, tags: [ 'vpn' ] } diff --git a/library/digital_ocean_tag.py b/library/digital_ocean_tag.py index b80d18b5..30a31852 100644 --- a/library/digital_ocean_tag.py +++ b/library/digital_ocean_tag.py @@ -1,26 +1,25 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# This file is part of Ansible -# -# Ansible 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. -# -# Ansible 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 Ansible. If not, see . +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + DOCUMENTATION = ''' --- module: digital_ocean_tag short_description: Create and remove tag(s) to DigitalOcean resource. description: - Create and remove tag(s) to DigitalOcean resource. +author: "Victor Volle (@kontrafiktion)" version_added: "2.2" options: name: @@ -31,9 +30,11 @@ options: resource_id: description: - The ID of the resource to operate on. + - The data type of resource_id is changed from integer to string, from version 2.5. + aliases: ['droplet_id'] resource_type: description: - - The type of resource to operate on. Currently only tagging of + - The type of resource to operate on. Currently, only tagging of droplets is supported. default: droplet choices: ['droplet'] @@ -65,7 +66,7 @@ EXAMPLES = ''' - name: tag a resource; creating the tag if it does not exists digital_ocean_tag: name: "{{ item }}" - resource_id: YYY + resource_id: "73333005" state: present with_items: - staging @@ -74,7 +75,7 @@ EXAMPLES = ''' - name: untag a resource digital_ocean_tag: name: staging - resource_id: YYY + resource_id: "73333005" state: absent # Deleting a tag also untags all the resources that have previously been @@ -104,133 +105,90 @@ data: } ''' -import json -import os - +from traceback import format_exc from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url - - -class Response(object): - - def __init__(self, resp, info): - self.body = None - if resp: - self.body = resp.read() - self.info = info - - @property - def json(self): - if not self.body: - if "body" in self.info: - return json.loads(self.info["body"]) - return None - try: - return json.loads(self.body) - except ValueError: - return None - - @property - def status_code(self): - return self.info["status"] - - -class Rest(object): - - def __init__(self, module, headers): - self.module = module - self.headers = headers - self.baseurl = 'https://api.digitalocean.com/v2' - - def _url_builder(self, path): - if path[0] == '/': - path = path[1:] - return '%s/%s' % (self.baseurl, path) - - def send(self, method, path, data=None, headers=None): - url = self._url_builder(path) - data = self.module.jsonify(data) - - resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method) - - return Response(resp, info) - - def get(self, path, data=None, headers=None): - return self.send('GET', path, data, headers) - - def put(self, path, data=None, headers=None): - return self.send('PUT', path, data, headers) - - def post(self, path, data=None, headers=None): - return self.send('POST', path, data, headers) - - def delete(self, path, data=None, headers=None): - return self.send('DELETE', path, data, headers) +from ansible.module_utils.digital_ocean import DigitalOceanHelper +from ansible.module_utils._text import to_native def core(module): - try: - api_token = module.params['api_token'] or \ - os.environ['DO_API_TOKEN'] or os.environ['DO_API_KEY'] - except KeyError as e: - module.fail_json(msg='Unable to load %s' % e.message) - state = module.params['state'] name = module.params['name'] resource_id = module.params['resource_id'] resource_type = module.params['resource_type'] - rest = Rest(module, {'Authorization': 'Bearer {}'.format(api_token), - 'Content-type': 'application/json'}) + rest = DigitalOceanHelper(module) - if state in ('present'): - if name is None: - module.fail_json(msg='parameter `name` is missing') - - # Ensure Tag exists - response = rest.post("tags", data={'name': name}) + # Check if api_token is valid or not + response = rest.get('account') + if response.status_code == 401: + module.fail_json(msg='Failed to login using api_token, please verify ' + 'validity of api_token') + if state == 'present': + response = rest.get('tags/{0}'.format(name)) status_code = response.status_code - json = response.json - if status_code == 201: - changed = True - elif status_code == 422: + resp_json = response.json + changed = False + if status_code == 200 and resp_json['tag']['name'] == name: changed = False else: - module.exit_json(changed=False, data=json) + # Ensure Tag exists + response = rest.post("tags", data={'name': name}) + status_code = response.status_code + resp_json = response.json + if status_code == 201: + changed = True + elif status_code == 422: + changed = False + else: + module.exit_json(changed=False, data=resp_json) if resource_id is None: # No resource defined, we're done. - if json is None: - module.exit_json(changed=changed, data=json) - else: - module.exit_json(changed=changed, data=json) + module.exit_json(changed=changed, data=resp_json) else: - # Tag a resource - url = "tags/{}/resources".format(name) - payload = { - 'resources': [{ - 'resource_id': resource_id, - 'resource_type': resource_type}]} - response = rest.post(url, data=payload) - if response.status_code == 204: - module.exit_json(changed=True) + # Check if resource is already tagged or not + found = False + url = "{0}?tag_name={1}".format(resource_type, name) + if resource_type == 'droplet': + url = "droplets?tag_name={0}".format(name) + response = rest.get(url) + status_code = response.status_code + resp_json = response.json + if status_code == 200: + for resource in resp_json['droplets']: + if not found and resource['id'] == int(resource_id): + found = True + break + if not found: + # If resource is not tagged, tag a resource + url = "tags/{0}/resources".format(name) + payload = { + 'resources': [{ + 'resource_id': resource_id, + 'resource_type': resource_type}]} + response = rest.post(url, data=payload) + if response.status_code == 204: + module.exit_json(changed=True) + else: + module.fail_json(msg="error tagging resource '{0}': {1}".format(resource_id, response.json["message"])) + else: + # Already tagged resource + module.exit_json(changed=False) else: - module.fail_json(msg="error tagging resource '{}': {}".format( - resource_id, response.json["message"])) - - elif state in ('absent'): - if name is None: - module.fail_json(msg='parameter `name` is missing') + # Unable to find resource specified by user + module.fail_json(msg=resp_json['message']) + elif state == 'absent': if resource_id: - url = "tags/{}/resources".format(name) + url = "tags/{0}/resources".format(name) payload = { 'resources': [{ 'resource_id': resource_id, 'resource_type': resource_type}]} response = rest.delete(url, data=payload) else: - url = "tags/{}".format(name) + url = "tags/{0}".format(name) response = rest.delete(url) if response.status_code == 204: module.exit_json(changed=True) @@ -252,7 +210,8 @@ def main(): try: core(module) except Exception as e: - module.fail_json(msg=str(e)) + module.fail_json(msg=to_native(e), exception=format_exc()) + if __name__ == '__main__': main() diff --git a/library/lightsail.py b/library/lightsail.py new file mode 100644 index 00000000..99e49ac7 --- /dev/null +++ b/library/lightsail.py @@ -0,0 +1,551 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: lightsail +short_description: Create or delete a virtual machine instance in AWS Lightsail +description: + - Creates or instances in AWS Lightsail and optionally wait for it to be 'running'. +version_added: "2.4" +author: "Nick Ball (@nickball)" +options: + state: + description: + - Indicate desired state of the target. + default: present + choices: ['present', 'absent', 'running', 'restarted', 'stopped'] + name: + description: + - Name of the instance + required: true + default : null + zone: + description: + - AWS availability zone in which to launch the instance. Required when state='present' + required: false + default: null + blueprint_id: + description: + - ID of the instance blueprint image. Required when state='present' + required: false + default: null + bundle_id: + description: + - Bundle of specification info for the instance. Required when state='present' + required: false + default: null + user_data: + description: + - Launch script that can configure the instance with additional data + required: false + default: null + key_pair_name: + description: + - Name of the key pair to use with the instance + required: false + default: null + wait: + description: + - Wait for the instance to be in state 'running' before returning. If wait is "no" an ip_address may not be returned + default: "yes" + choices: [ "yes", "no" ] + wait_timeout: + description: + - How long before wait gives up, in seconds. + default: 300 + open_ports: + description: + - Adds public ports to an Amazon Lightsail instance. + default: null + suboptions: + from_port: + description: Begin of the range + required: true + default: null + to_port: + description: End of the range + required: true + default: null + protocol: + description: Accepted traffic protocol. + required: true + choices: + - udp + - tcp + - all + default: null +requirements: + - "python >= 2.6" + - boto3 + +extends_documentation_fragment: + - aws + - ec2 +''' + + +EXAMPLES = ''' +# Create a new Lightsail instance, register the instance details +- lightsail: + state: present + name: myinstance + region: us-east-1 + zone: us-east-1a + blueprint_id: ubuntu_16_04 + bundle_id: nano_1_0 + key_pair_name: id_rsa + user_data: " echo 'hello world' > /home/ubuntu/test.txt" + wait_timeout: 500 + open_ports: + - from_port: 4500 + to_port: 4500 + protocol: udp + - from_port: 500 + to_port: 500 + protocol: udp + register: my_instance + +- debug: + msg: "Name is {{ my_instance.instance.name }}" + +- debug: + msg: "IP is {{ my_instance.instance.publicIpAddress }}" + +# Delete an instance if present +- lightsail: + state: absent + region: us-east-1 + name: myinstance + +''' + +RETURN = ''' +changed: + description: if a snapshot has been modified/created + returned: always + type: bool + sample: + changed: true +instance: + description: instance data + returned: always + type: dict + sample: + arn: "arn:aws:lightsail:us-east-1:448830907657:Instance/1fef0175-d6c8-480e-84fa-214f969cda87" + blueprint_id: "ubuntu_16_04" + blueprint_name: "Ubuntu" + bundle_id: "nano_1_0" + created_at: "2017-03-27T08:38:59.714000-04:00" + hardware: + cpu_count: 1 + ram_size_in_gb: 0.5 + is_static_ip: false + location: + availability_zone: "us-east-1a" + region_name: "us-east-1" + name: "my_instance" + networking: + monthly_transfer: + gb_per_month_allocated: 1024 + ports: + - access_direction: "inbound" + access_from: "Anywhere (0.0.0.0/0)" + access_type: "public" + common_name: "" + from_port: 80 + protocol: tcp + to_port: 80 + - access_direction: "inbound" + access_from: "Anywhere (0.0.0.0/0)" + access_type: "public" + common_name: "" + from_port: 22 + protocol: tcp + to_port: 22 + private_ip_address: "172.26.8.14" + public_ip_address: "34.207.152.202" + resource_type: "Instance" + ssh_key_name: "keypair" + state: + code: 16 + name: running + support_code: "588307843083/i-0997c97831ee21e33" + username: "ubuntu" +''' + +import time +import traceback + +try: + import botocore + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 +except ImportError: + # will be caught by imported HAS_BOTO3 + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, + HAS_BOTO3, camel_dict_to_snake_dict) + + +def create_instance(module, client, instance_name): + """ + Create an instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to delete + + Returns a dictionary of instance information + about the new instance. + + """ + + changed = False + + # Check if instance already exists + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + zone = module.params.get('zone') + blueprint_id = module.params.get('blueprint_id') + bundle_id = module.params.get('bundle_id') + user_data = module.params.get('user_data') + user_data = '' if user_data is None else user_data + + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + if module.params.get('key_pair_name'): + key_pair_name = module.params.get('key_pair_name') + else: + key_pair_name = '' + + if module.params.get('open_ports'): + open_ports = module.params.get('open_ports') + else: + open_ports = '[]' + + resp = None + if inst is None: + try: + resp = client.create_instances( + instanceNames=[ + instance_name + ], + availabilityZone=zone, + blueprintId=blueprint_id, + bundleId=bundle_id, + userData=user_data, + keyPairName=key_pair_name, + ) + resp = resp['operations'][0] + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Unable to create instance {0}, error: {1}'.format(instance_name, e)) + + inst = _find_instance_info(client, instance_name) + + # Wait for instance to become running + if wait: + while (wait_max > time.time()) and (inst is not None and inst['state']['name'] != "running"): + try: + time.sleep(2) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to start instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(1) + + # Timed out + if wait and not changed and wait_max <= time.time(): + module.fail_json(msg="Wait for instance start timeout at %s" % time.asctime()) + + # Attempt to open ports + if open_ports: + if inst is not None: + try: + for o in open_ports: + resp = client.open_instance_public_ports( + instanceName=instance_name, + portInfo={ + 'fromPort': o['from_port'], + 'toPort': o['to_port'], + 'protocol': o['protocol'] + } + ) + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Error opening ports for instance {0}, error: {1}'.format(instance_name, e)) + + changed = True + + return (changed, inst) + + +def delete_instance(module, client, instance_name): + """ + Terminates an instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to delete + + Returns a dictionary of instance information + about the instance deleted (pre-deletion). + + If the instance to be deleted is running + "changed" will be set to False. + + """ + + # It looks like deleting removes the instance immediately, nothing to wait for + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before deleting + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to delete instance {0}. Check that you have permissions to perform the operation.".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to delete instance {0}.".format(instance_name), exception=traceback.format_exc()) + # sleep and retry + time.sleep(10) + + # Attempt to delete + if inst is not None: + while not changed and ((wait and wait_max > time.time()) or (not wait)): + try: + client.delete_instance(instanceName=instance_name) + changed = True + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Error deleting instance {0}, error: {1}'.format(instance_name, e)) + + # Timed out + if wait and not changed and wait_max <= time.time(): + module.fail_json(msg="wait for instance delete timeout at %s" % time.asctime()) + + return (changed, inst) + + +def restart_instance(module, client, instance_name): + """ + Reboot an existing instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to reboot + + Returns a dictionary of instance information + about the restarted instance + + If the instance was not able to reboot, + "changed" will be set to False. + + Wait will not apply here as this is an OS-level operation + """ + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before state change + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to restart instance {0}. Check that you have permissions to perform the operation.".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to restart instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(3) + + # send reboot + if inst is not None: + try: + client.reboot_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Unable to reboot instance {0}, error: {1}'.format(instance_name, e)) + changed = True + + return (changed, inst) + + +def startstop_instance(module, client, instance_name, state): + """ + Starts or stops an existing instance + + module: Ansible module object + client: authenticated lightsail connection object + instance_name: name of instance to start/stop + state: Target state ("running" or "stopped") + + Returns a dictionary of instance information + about the instance started/stopped + + If the instance was not able to state change, + "changed" will be set to False. + + """ + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + wait_max = time.time() + wait_timeout + + changed = False + + inst = None + try: + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] != 'NotFoundException': + module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) + + # Wait for instance to exit transition state before state change + if wait: + while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): + try: + time.sleep(5) + inst = _find_instance_info(client, instance_name) + except botocore.exceptions.ClientError as e: + if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": + module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), + exception=traceback.format_exc()) + elif e.response['Error']['Code'] == "RequestExpired": + module.fail_json(msg="RequestExpired: Failed to start/stop instance {0}.".format(instance_name), exception=traceback.format_exc()) + time.sleep(1) + + # Try state change + if inst is not None and inst['state']['name'] != state: + try: + if state == 'running': + client.start_instance(instanceName=instance_name) + else: + client.stop_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(instance_name, e)) + changed = True + # Grab current instance info + inst = _find_instance_info(client, instance_name) + + return (changed, inst) + + +def core(module): + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + if not region: + module.fail_json(msg='region must be specified') + + client = None + try: + client = boto3_conn(module, conn_type='client', resource='lightsail', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg='Failed while connecting to the lightsail service: %s' % e, exception=traceback.format_exc()) + + changed = False + state = module.params['state'] + name = module.params['name'] + + if state == 'absent': + changed, instance_dict = delete_instance(module, client, name) + elif state in ('running', 'stopped'): + changed, instance_dict = startstop_instance(module, client, name, state) + elif state == 'restarted': + changed, instance_dict = restart_instance(module, client, name) + elif state == 'present': + changed, instance_dict = create_instance(module, client, name) + + module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(instance_dict)) + + +def _find_instance_info(client, instance_name): + ''' handle exceptions where this function is called ''' + inst = None + try: + inst = client.get_instance(instanceName=instance_name) + except botocore.exceptions.ClientError as e: + raise + return inst['instance'] + + +def main(): + argument_spec = ec2_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['present', 'absent', 'stopped', 'running', 'restarted']), + zone=dict(type='str'), + blueprint_id=dict(type='str'), + bundle_id=dict(type='str'), + key_pair_name=dict(type='str'), + user_data=dict(type='str'), + wait=dict(type='bool', default=True), + wait_timeout=dict(default=300), + open_ports=dict(type='list') + )) + + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + if not HAS_BOTOCORE: + module.fail_json(msg='Python module "botocore" is missing, please install it') + + try: + core(module) + except (botocore.exceptions.ClientError, Exception) as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/playbooks/common.yml b/playbooks/common.yml index 04a3966c..5628c37f 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -5,11 +5,11 @@ register: OS - name: Ubuntu pre-tasks - include: ubuntu.yml + include_tasks: ubuntu.yml when: '"Ubuntu" in OS.stdout' - name: FreeBSD pre-tasks - include: freebsd.yml + include_tasks: freebsd.yml when: '"FreeBSD" in OS.stdout' -- include: facts/main.yml +- include_tasks: facts/main.yml diff --git a/playbooks/freebsd.yml b/playbooks/freebsd.yml index 8cf0579f..316c92ac 100644 --- a/playbooks/freebsd.yml +++ b/playbooks/freebsd.yml @@ -6,4 +6,4 @@ - name: FreeBSD / HardenedBSD | Configure defaults raw: sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7 -- include: facts/FreeBSD.yml +- include_tasks: facts/FreeBSD.yml diff --git a/playbooks/post.yml b/playbooks/post.yml index f9f41983..e594b973 100644 --- a/playbooks/post.yml +++ b/playbooks/post.yml @@ -13,4 +13,4 @@ pause: seconds: 20 -- include: local_ssh.yml +- include_tasks: local_ssh.yml diff --git a/requirements.txt b/requirements.txt index 67ec4a10..e7443ab0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,11 @@ -msrestazure setuptools>=11.3 -ansible>=2.1,<2.2.1 +ansible[azure]==2.4.3 dopy==0.3.5 boto>=2.5 boto3 -azure==2.0.0rc5 -msrest==0.4.1 apache-libcloud six pyopenssl jinja2==2.8 +shade +pycrypto diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 68397148..0a3eedce 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -2,7 +2,7 @@ setup: - name: Include system based facts and tasks - include: systems/main.yml + include_tasks: systems/main.yml - name: Install prerequisites package: name="{{ item }}" state=present diff --git a/roles/client/tasks/systems/main.yml b/roles/client/tasks/systems/main.yml index 85da1ebd..ba24c939 100644 --- a/roles/client/tasks/systems/main.yml +++ b/roles/client/tasks/systems/main.yml @@ -1,13 +1,13 @@ --- -- include: Debian.yml +- include_tasks: Debian.yml when: ansible_distribution == 'Debian' -- include: Ubuntu.yml +- include_tasks: Ubuntu.yml when: ansible_distribution == 'Ubuntu' -- include: CentOS.yml +- include_tasks: CentOS.yml when: ansible_distribution == 'CentOS' -- include: Fedora.yml +- include_tasks: Fedora.yml when: ansible_distribution == 'Fedora' diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index e32e70a5..7d5894c7 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -19,10 +19,10 @@ - set_fact: ami_image: "{{ ami_search.results[0].ami_id }}" - - include: encrypt_image.yml + - include_tasks: encrypt_image.yml tags: [encrypted] - - include: cloudformation.yml + - include_tasks: cloudformation.yml - name: Add new instance to host group add_host: @@ -38,7 +38,7 @@ cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" - name: Get EC2 instances - ec2_remote_facts: + ec2_instance_facts: aws_access_key: "{{ access_key }}" aws_secret_key: "{{ secret_key }}" region: "{{ region }}" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml new file mode 100644 index 00000000..ce28ceb4 --- /dev/null +++ b/roles/cloud-lightsail/tasks/main.yml @@ -0,0 +1,52 @@ +- block: + - set_fact: + access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + region: "{{ algo_region | default(lookup('env','AWS_DEFAULT_REGION'), true) }}" + + - name: Create an instance + lightsail: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + name: "{{ algo_server_name }}" + state: present + region: "{{ region }}" + zone: "{{ region }}a" + blueprint_id: "{{ cloud_providers.lightsail.image }}" + bundle_id: "{{ cloud_providers.lightsail.size }}" + wait_timeout: 300 + open_ports: + - from_port: 4500 + to_port: 4500 + protocol: udp + - from_port: 500 + to_port: 500 + protocol: udp + user_data: | + #!/bin/bash + mkdir -p /home/ubuntu/.ssh/ + echo "{{ lookup('file', '{{ SSH_keys.public }}') }}" >> /home/ubuntu/.ssh/authorized_keys + chown -R ubuntu: /home/ubuntu/.ssh/ + chmod 0700 /home/ubuntu/.ssh/ + chmod 0600 /home/ubuntu/.ssh/* + test + register: algo_instance + + - set_fact: + cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" + + - name: Add new instance to host group + add_host: + hostname: "{{ cloud_instance_ip }}" + groupname: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: lightsail + ipv6_support: no + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml new file mode 100644 index 00000000..aef49a5b --- /dev/null +++ b/roles/cloud-openstack/tasks/main.yml @@ -0,0 +1,87 @@ +--- +- block: + - name: Security group created + os_security_group: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}-security_group" + description: AlgoVPN security group + register: os_security_group + + - name: Security rules created + os_security_group_rule: + state: "{{ state|default('present') }}" + security_group: "{{ os_security_group.id }}" + protocol: "{{ item.proto }}" + port_range_min: "{{ item.port_min }}" + port_range_max: "{{ item.port_max }}" + remote_ip_prefix: "{{ item.range }}" + with_items: + - { proto: tcp, port_min: 22, port_max: 22, range: 0.0.0.0/0 } + - { proto: icmp, port_min: -1, port_max: -1, range: 0.0.0.0/0 } + - { proto: udp, port_min: 4500, port_max: 4500, range: 0.0.0.0/0 } + - { proto: udp, port_min: 500, port_max: 500, range: 0.0.0.0/0 } + + - name: Keypair created + os_keypair: + state: "{{ state|default('present') }}" + name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" + public_key_file: "{{ SSH_keys.public }}" + register: os_keypair + + - name: Gather facts about flavors + os_flavor_facts: + ram: "{{ cloud_providers.openstack.flavor_ram }}" + + - name: Gather facts about images + os_image_facts: + image: "{{ cloud_providers.openstack.image }}" + + - name: Gather facts about public networks + os_networks_facts: + + - name: Set the network as a fact + set_fact: + public_network_id: "{{ item.id }}" + when: + - item['router:external']|default(omit) + - item['admin_state_up']|default(omit) + - item['status'] == 'ACTIVE' + with_items: "{{ openstack_networks }}" + + - name: Set facts + set_fact: + flavor_id: "{{ (openstack_flavors | sort(attribute='ram'))[0]['id'] }}" + image_id: "{{ openstack_image['id'] }}" + keypair_name: "{{ os_keypair.key.name }}" + security_group_name: "{{ os_security_group['secgroup']['name'] }}" + + - name: Server created + os_server: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}" + image: "{{ image_id }}" + flavor: "{{ flavor_id }}" + key_name: "{{ keypair_name }}" + security_groups: "{{ security_group_name }}" + nics: + - net-id: "{{ public_network_id }}" + register: os_server + + - set_fact: + cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" + + - name: Add new instance to host group + add_host: + hostname: "{{ cloud_instance_ip }}" + groupname: vpn-host + ansible_ssh_user: ubuntu + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: openstack + ipv6_support: omit + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml new file mode 100644 index 00000000..ca4e4e6d --- /dev/null +++ b/roles/cloud-scaleway/tasks/main.yml @@ -0,0 +1,128 @@ +- block: + - name: Check if server exists + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + status_code: 200 + register: scaleway_servers + + - name: Set server id as a fact + set_fact: + server_id: "{{ item.id }}" + no_log: true + when: algo_server_name == item.name + with_items: "{{ scaleway_servers.json.servers }}" + + - name: Create a server if it doesn't exist + block: + - name: Get the organization id + uri: + url: https://account.cloud.online.net/organizations + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + status_code: 200 + register: scaleway_organizations + + - name: Set organization id as a fact + set_fact: + organization_id: "{{ item.id }}" + no_log: true + when: scaleway_organization == item.name + with_items: "{{ scaleway_organizations.json.organizations }}" + + - name: Get images + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/images" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + status_code: 200 + register: scaleway_images + + - name: Set image id as a fact + set_fact: + image_id: "{{ item.id }}" + no_log: true + when: + - cloud_providers.scaleway.image in item.name + - cloud_providers.scaleway.arch == item.arch + with_items: "{{ scaleway_images.json.images }}" + + - name: Create a server + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers/" + method: POST + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + body: + organization: "{{ organization_id }}" + name: "{{ algo_server_name }}" + image: "{{ image_id }}" + commercial_type: "{{cloud_providers.scaleway.size }}" + tags: + - Environment:Algo + - AUTHORIZED_KEY={{ lookup('file', SSH_keys.public)|regex_replace(' ', '_') }} + enable_ipv6: true + status_code: 201 + body_format: json + register: algo_instance + + - name: Set server id as a fact + set_fact: + server_id: "{{ algo_instance.json.server.id }}" + when: server_id is not defined + + - name: Power on the server + uri: + url: https://cp-{{ algo_region }}.scaleway.com/servers/{{ server_id }}/action + method: POST + headers: + Content-Type: application/json + X-Auth-Token: "{{ scaleway_auth_token }}" + body: + action: poweron + status_code: 202 + body_format: json + ignore_errors: true + no_log: true + + - name: Wait for the server to become running + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/servers/{{ server_id }}" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + status_code: 200 + until: + - algo_instance.json.server.state is defined + - algo_instance.json.server.state == "running" + retries: 20 + delay: 30 + register: algo_instance + + - set_fact: + cloud_instance_ip: "{{ algo_instance['json']['server']['public_ip']['address'] }}" + + - name: Add new instance to host group + add_host: + hostname: "{{ cloud_instance_ip }}" + groupname: vpn-host + ansible_ssh_user: root + ansible_python_interpreter: "/usr/bin/python2.7" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + cloud_provider: scaleway + ipv6_support: yes + + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 781930e2..5b6aa438 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,9 +1,9 @@ --- - block: - - include: ubuntu.yml + - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include: freebsd.yml + - include_tasks: freebsd.yml when: ansible_distribution == 'FreeBSD' - name: Install tools diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 4c5705e4..ce337741 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -1,46 +1,42 @@ --- +- name: Cloud only tasks + block: + - name: Install software updates + apt: update_cache=yes upgrade=dist -- name: Install software updates - apt: update_cache=yes upgrade=dist - tags: - - cloud + - name: Check if reboot is required + shell: > + if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi + args: + executable: /bin/bash + register: reboot_required -- name: Check if reboot is required - shell: > - if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi - args: - executable: /bin/bash - register: reboot_required - tags: - - cloud + - name: Reboot + shell: sleep 2 && shutdown -r now "Ansible updates triggered" + async: 1 + poll: 0 + when: reboot_required is defined and reboot_required.stdout == 'required' + ignore_errors: true -- name: Reboot - shell: sleep 2 && shutdown -r now "Ansible updates triggered" - async: 1 - poll: 0 - when: reboot_required is defined and reboot_required.stdout == 'required' - ignore_errors: true - tags: - - cloud + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ inventory_hostname }}" + search_regex: OpenSSH + delay: 10 + timeout: 320 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false -- name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ inventory_hostname }}" - search_regex: OpenSSH - delay: 10 - timeout: 320 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false - tags: - - cloud + - name: Include unatteded upgrades configuration + include_tasks: unattended-upgrades.yml -- name: Disable MOTD on login and SSHD - replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" - with_items: - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } + - name: Disable MOTD on login and SSHD + replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" + with_items: + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } tags: - cloud diff --git a/roles/common/tasks/unattended-upgrades.yml b/roles/common/tasks/unattended-upgrades.yml new file mode 100644 index 00000000..378c16e3 --- /dev/null +++ b/roles/common/tasks/unattended-upgrades.yml @@ -0,0 +1,21 @@ +--- +- name: Install unattended-upgrades + apt: + name: unattended-upgrades + state: latest + +- name: Configure unattended-upgrades + template: + src: 50unattended-upgrades.j2 + dest: /etc/apt/apt.conf.d/50unattended-upgrades + owner: root + group: root + mode: 0644 + +- name: Periodic upgrades configured + template: + src: 10periodic.j2 + dest: /etc/apt/apt.conf.d/10periodic + owner: root + group: root + mode: 0644 diff --git a/roles/security/templates/10periodic.j2 b/roles/common/templates/10periodic.j2 similarity index 76% rename from roles/security/templates/10periodic.j2 rename to roles/common/templates/10periodic.j2 index 75870203..5d37e9fc 100644 --- a/roles/security/templates/10periodic.j2 +++ b/roles/common/templates/10periodic.j2 @@ -1,4 +1,4 @@ APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; -APT::Periodic::Unattended-Upgrade "1"; \ No newline at end of file +APT::Periodic::Unattended-Upgrade "1"; diff --git a/roles/security/templates/50unattended-upgrades.j2 b/roles/common/templates/50unattended-upgrades.j2 similarity index 97% rename from roles/security/templates/50unattended-upgrades.j2 rename to roles/common/templates/50unattended-upgrades.j2 index 5f8fb159..0c55b702 100644 --- a/roles/security/templates/50unattended-upgrades.j2 +++ b/roles/common/templates/50unattended-upgrades.j2 @@ -15,7 +15,7 @@ Unattended-Upgrade::Package-Blacklist { }; // This option allows you to control if on a unclean dpkg exit -// unattended-upgrades will automatically run +// unattended-upgrades will automatically run // dpkg --force-confold --configure -a // The default is true, to ensure updates keep getting installed //Unattended-Upgrade::AutoFixInterruptedDpkg "false"; @@ -46,7 +46,7 @@ Unattended-Upgrade::Package-Blacklist { //Unattended-Upgrade::Remove-Unused-Dependencies "false"; // Automatically reboot *WITHOUT CONFIRMATION* -// if the file /var/run/reboot-required is found after the upgrade +// if the file /var/run/reboot-required is found after the upgrade //Unattended-Upgrade::Automatic-Reboot "false"; // If automatic reboot is enabled and needed, reboot at the specific diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 2ba74b77..43c06d5a 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -14,10 +14,10 @@ - name: The dnsmasq directory created file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup - - include: ubuntu.yml + - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include: freebsd.yml + - include_tasks: freebsd.yml when: ansible_distribution == 'FreeBSD' - name: Dnsmasq configured diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml deleted file mode 100644 index ab98db63..00000000 --- a/roles/security/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: restart ssh - service: name="{{ ssh_service_name|default('ssh') }}" state=restarted - -- name: flush routing cache - shell: echo 1 > /proc/sys/net/ipv4/route/flush diff --git a/roles/security/meta/main.yml b/roles/security/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/security/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml deleted file mode 100644 index 2f279122..00000000 --- a/roles/security/tasks/main.yml +++ /dev/null @@ -1,161 +0,0 @@ ---- -- block: - - name: Install tools - apt: name="{{ item }}" state=latest - with_items: - - unattended-upgrades - - - name: Configure unattended-upgrades - template: - src: 50unattended-upgrades.j2 - dest: /etc/apt/apt.conf.d/50unattended-upgrades - owner: root - group: root - mode: 0644 - - - name: Periodic upgrades configured - template: - src: 10periodic.j2 - dest: /etc/apt/apt.conf.d/10periodic - owner: root - group: root - mode: 0644 - - - name: Find directories for minimizing access - stat: - path: "{{ item }}" - register: minimize_access_directories - with_items: - - '/usr/local/sbin' - - '/usr/local/bin' - - '/usr/sbin' - - '/usr/bin' - - '/sbin' - - '/bin' - - - name: Minimize access - file: - path: '{{ item.stat.path }}' - mode: 'go-w' - recurse: yes - when: item.stat.isdir - with_items: "{{ minimize_access_directories.results }}" - no_log: True - - - name: Change shadow ownership to root and mode to 0600 - file: - dest: '/etc/shadow' - owner: root - group: root - mode: 0600 - - - name: change su-binary to only be accessible to user and group root - file: - dest: '/bin/su' - owner: root - group: root - mode: 0750 - - # Core dumps - - - name: Restrict core dumps (with PAM) - lineinfile: - dest: /etc/security/limits.conf - line: "* hard core 0" - state: present - - - name: Restrict core dumps (with sysctl) - sysctl: - name: fs.suid_dumpable - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - - # Kernel fixes - - - name: Disable Source Routed Packet Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.accept_source_route - - net.ipv4.conf.default.accept_source_route - notify: - - flush routing cache - - - name: Disable ICMP Redirect Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.accept_redirects - - net.ipv4.conf.default.accept_redirects - - - name: Disable Secure ICMP Redirect Acceptance - sysctl: - name: "{{item}}" - value: 0 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.secure_redirects - - net.ipv4.conf.default.secure_redirects - notify: - - flush routing cache - - - name: Enable Bad Error Message Protection - sysctl: - name: net.ipv4.icmp_ignore_bogus_error_responses - value: 1 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - notify: - - flush routing cache - - - name: Enable RFC-recommended Source Route Validation - sysctl: - name: "{{item}}" - value: 1 - ignoreerrors: yes - sysctl_set: yes - reload: yes - state: present - with_items: - - net.ipv4.conf.all.rp_filter - - net.ipv4.conf.default.rp_filter - notify: - - flush routing cache - - - name: Do not send ICMP redirects (we are not a router) - sysctl: - name: net.ipv4.conf.all.send_redirects - value: 0 - - - name: SSH config - template: - src: sshd_config.j2 - dest: /etc/ssh/sshd_config - owner: root - group: root - mode: 0644 - notify: - - restart ssh - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always diff --git a/roles/security/templates/sshd_config.j2 b/roles/security/templates/sshd_config.j2 deleted file mode 100644 index 4bdb2601..00000000 --- a/roles/security/templates/sshd_config.j2 +++ /dev/null @@ -1,51 +0,0 @@ -Port 22 -# ListenAddress :: -# ListenAddress 0.0.0.0 -Protocol 2 - -# LogLevel VERBOSE logs user's key fingerprint on login. -# Needed to have a clear audit log of which keys were used to log in. -SyslogFacility AUTH -LogLevel VERBOSE - -# Use kernel sandbox mechanisms where possible -# Systrace on OpenBSD, Seccomp on Linux, seatbelt on macOS X (Darwin), rlimit elsewhere. -UsePrivilegeSeparation sandbox - -# Handy for keeping network connections alive -TCPKeepAlive yes -ClientAliveInterval 120 - -# Authentication -UsePAM yes -PermitRootLogin without-password -StrictModes yes -PubkeyAuthentication yes -AcceptEnv LANG LC_* - -# Turn off a lot of features -IgnoreRhosts yes -HostbasedAuthentication no -PermitEmptyPasswords no -ChallengeResponseAuthentication no -PasswordAuthentication no -UseDNS no - -# Do not enable sftp -# If you DO enable it, use this line to log which files sftp users read/write -# Subsystem sftp /usr/lib/ssh/sftp-server -f AUTHPRIV -l INFO - -# This makes ansible faster -PrintMotd no -PrintLastLog yes - -# Use only modern host keys -HostKey /etc/ssh/ssh_host_ed25519_key -HostKey /etc/ssh/ssh_host_ecdsa_key - -# Use only modern ciphers -KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256 -Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com -MACs hmac-sha2-256-etm@openssh.com -HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256 -# PubkeyAcceptedKeyTypes accept anything diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 8e732e1d..e0d0d1bf 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -6,20 +6,20 @@ - name: Ensure that the strongswan user exist user: name=strongswan group=strongswan state=present - - include: ubuntu.yml + - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include: freebsd.yml + - include_tasks: freebsd.yml when: ansible_distribution == 'FreeBSD' - name: Install strongSwan package: name=strongswan state=present - - include: ipec_configuration.yml - - include: openssl.yml + - include_tasks: ipec_configuration.yml + - include_tasks: openssl.yml tags: update-users - - include: distribute_keys.yml - - include: client_configs.yml + - include_tasks: distribute_keys.yml + - include_tasks: client_configs.yml delegate_to: localhost become: no tags: update-users diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index ccc561b3..d3a858ca 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -44,5 +44,5 @@ - daemon-reload - restart strongswan -- include: iptables.yml +- include_tasks: iptables.yml tags: iptables diff --git a/users.yml b/users.yml index 92792085..46a2d79c 100644 --- a/users.yml +++ b/users.yml @@ -45,7 +45,7 @@ pre_tasks: - block: - name: Common pre-tasks - include: playbooks/common.yml + include_tasks: playbooks/common.yml tags: always rescue: - debug: var=fail_hint From 7e07c354744c8467890f74d1a63efed259585ed5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 3 Mar 2018 00:13:49 +0300 Subject: [PATCH 580/769] proper cloudformation template (#815) --- .../stack.yml.j2 => files/stack.yml} | 27 ++++++++++++------- roles/cloud-ec2/tasks/cloudformation.yml | 14 +++++----- 2 files changed, 24 insertions(+), 17 deletions(-) rename roles/cloud-ec2/{templates/stack.yml.j2 => files/stack.yml} (88%) diff --git a/roles/cloud-ec2/templates/stack.yml.j2 b/roles/cloud-ec2/files/stack.yml similarity index 88% rename from roles/cloud-ec2/templates/stack.yml.j2 rename to roles/cloud-ec2/files/stack.yml index 694386f8..7f814e35 100644 --- a/roles/cloud-ec2/templates/stack.yml.j2 +++ b/roles/cloud-ec2/files/stack.yml @@ -1,13 +1,19 @@ --- - AWSTemplateFormatVersion: '2010-09-09' Description: 'Algo VPN stack' +Parameters: + InstanceTypeParameter: + Type: String + Default: t2.micro + PublicSSHKeyParameter: + Type: String + ImageIdParameter: + Type: String Resources: - VPC: Type: AWS::EC2::VPC Properties: - CidrBlock: {{ ec2_vpc_nets.cidr_block }} + CidrBlock: 172.16.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default @@ -35,7 +41,7 @@ Resources: Subnet: Type: AWS::EC2::Subnet Properties: - CidrBlock: {{ ec2_vpc_nets.subnet_cidr }} + CidrBlock: 172.16.254.0/23 MapPublicIpOnLaunch: false Tags: - Key: Environment @@ -148,16 +154,19 @@ Resources: homeDir: "/home/ubuntu/" files: /home/ubuntu/.ssh/authorized_keys: - content: {{ lookup('file', SSH_keys.public) }} + content: + Ref: PublicSSHKeyParameter mode: "000644" owner: "ubuntu" group: "ubuntu" Properties: - InstanceType: {{ cloud_providers.ec2.size }} + InstanceType: + Ref: InstanceTypeParameter InstanceInitiatedShutdownBehavior: terminate SecurityGroupIds: - Ref: InstanceSecurityGroup - ImageId: {{ ami_image }} + ImageId: + Ref: ImageIdParameter SubnetId: !Ref Subnet Ipv6AddressCount: 1 UserData: @@ -176,8 +185,8 @@ Resources: apt-get update apt-get -y install python-setuptools easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz - cfn-init -v --stack {{ stack_name }} --resource EC2Instance --region {{ region }} - cfn-signal -e $? --stack {{ stack_name }} --resource EC2Instance --region {{ region }} + cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} + cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} Tags: - Key: Name Value: Algo diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml index 1f24b007..032a59b6 100644 --- a/roles/cloud-ec2/tasks/cloudformation.yml +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -1,10 +1,4 @@ --- - -- name: Make a cloudformation template - template: - src: stack.yml.j2 - dest: "configs/{{ aws_server_name }}.yml" - - name: Deploy the template cloudformation: aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" @@ -12,7 +6,11 @@ stack_name: "{{ stack_name }}" state: "present" region: "{{ region }}" - template: "configs/{{ aws_server_name }}.yml" + template: roles/cloud-ec2/files/stack.yml + template_parameters: + InstanceTypeParameter: "{{ cloud_providers.ec2.size }}" + PublicSSHKeyParameter: "{{ lookup('file', SSH_keys.public) }}" + ImageIdParameter: "{{ ami_image }}" tags: Environment: Algo - register: stack \ No newline at end of file + register: stack From ea7da89257c5b4fa69d3ecca7ef23747fea3e8ba Mon Sep 17 00:00:00 2001 From: Berry Phillips Date: Fri, 9 Mar 2018 12:16:40 +0900 Subject: [PATCH 581/769] Explicitly create the virtualenv with Python2 (#823) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55b1bd6f..a740731f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. **Install Algo's remaining dependencies.** Use the same Terminal window as the previous step and run: ```bash - $ python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt + $ python -m virtualenv --python=`which python2` env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. From 13503575c55437d6de0d07d9a0968b4610bec141 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 12 Mar 2018 10:29:18 +0300 Subject: [PATCH 582/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 48 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a7c92bf1..a34aab3e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,38 +1,36 @@ -### OS / Environment - - - -### Ansible version - - - -### Version of components from `requirements.txt` +### OS / Environment (where do you run Algo on) +``` +PUT THE OUTPUT HERE +``` + +### Cloud Provider or OS / Environment (where do you deploy Algo to) + + +``` +PUT THE OUTPUT HERE +``` + ### Summary of the problem - + ### Steps to reproduce the behavior + - - -### The way of deployment (cloud or local) - - - -### Expected behavior - - - -### Actual behavior - - +1. Do this.. +2. Do that.. +3. ### Full log +``` +PUT THE OUTPUT HERE +``` From 3bb6c32abb4e034a004ee2ee15e53b9e9a7ffc57 Mon Sep 17 00:00:00 2001 From: Zac Connelly Date: Mon, 12 Mar 2018 16:49:45 +0100 Subject: [PATCH 583/769] update troubleshooting doc (#827) * update troubleshooting doc * remove breakline * bump issue to the bottom --- docs/troubleshooting.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a862ac26..2c860a58 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -63,7 +63,7 @@ checking for gcc... gcc checking whether the C compiler works... no configure: error: in '/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto': configure: error: C compiler cannot create executables See config.log for more details Traceback (most recent call last): -File "", line 1, in +File "", line 1, in ... cmd_obj.run() File "/private/var/folders/3f/q33hl6_x6_nfyjg29fcl9qdr0000gp/T/pip-build-DB5VZp/pycrypto/setup.py", line 278, in run @@ -94,7 +94,7 @@ Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/p Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. +You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. ### Error: "TypeError: must be str, not bytes" @@ -215,6 +215,32 @@ On Windows, this issue may manifest with an error message that says "The network It is possible that the IKE_AUTH payload is too big to fit in a single IP datagram, and so is fragmented. Many consumer routers and cable modems ship with a feature that blocks "fragmented IP packets." Try logging into your router and disabling any firewall settings related to blocking or dropping fragmented IP packets. For more information, see [Issue #305](https://github.com/trailofbits/algo/issues/305). +### Error: name 'basestring' is not defined + +``` +TASK [cloud-digitalocean : Creating a droplet...] ******************************************* +An exception occurred during task execution. To see the full traceback, use -vvv. The error was: NameError: name 'basestring' is not defined +fatal: [localhost]: FAILED! => {"changed": false, "msg": "name 'basestring' is not defined"} +``` + +If you get something like the above it's likely you're not using a python2 virtualenv. + +Ensure running `python2.7` drops you into a python 2 shell (it looks something like this) + +``` +user@homebook ~ $ python2.7 +Python 2.7.10 (default, Feb 7 2017, 00:08:15) +[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +Then rerun the dependency installation explicitly using python 2.7 + +``` +python2.7 -m virtualenv --python=`which python2.7` env && source env/bin/activate && python2.7 -m pip install -U pip && python2.7 -m pip install -r requirements.txt +``` + ## I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Gitter](https://gitter.im/trailofbits/algo) and ask for help. If you think you found a new issue in Algo, [file an issue](https://github.com/trailofbits/algo/issues/new). From b30f6db0796652d6791b1913fe23a54b0bc54f49 Mon Sep 17 00:00:00 2001 From: adamluk Date: Mon, 12 Mar 2018 15:51:34 +0000 Subject: [PATCH 584/769] Update rules.v6.j2 (#818) Updated to use -m conntrack for consistency as per the other IPv6 rules. --- roles/vpn/templates/rules.v6.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 640f6d29..717b887f 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -32,7 +32,7 @@ COMMIT -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT # DHCP in AWS --A INPUT -m state --state NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT +-A INPUT -m conntrack --ctstate NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any From 3b19f1308282913bb6ab645bfad3de2846cf4ffd Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 12 Mar 2018 19:00:48 +0300 Subject: [PATCH 585/769] Enable no-resolv (#816) --- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 3424d544..f92ee163 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -55,7 +55,7 @@ # If you don't want dnsmasq to read /etc/resolv.conf or any other # file, getting its servers from this file instead (see below), then # uncomment this. -#no-resolv +no-resolv # If you don't want dnsmasq to poll /etc/resolv.conf or other resolv # files for changes and re-read them then uncomment this. From 72026d0ec87c1b5e6e370ba8ca7be3e2ec26e501 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 16 Mar 2018 21:01:26 +0300 Subject: [PATCH 586/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a34aab3e..6aa68af3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,15 +7,13 @@ Run the command `uname -a` and put the output here PUT THE OUTPUT HERE ``` -### Cloud Provider or OS / Environment (where do you deploy Algo to) +### Cloud Provider (where do you deploy Algo to) ``` PUT THE OUTPUT HERE ``` - ### Summary of the problem From 0fda81f12df9b332a259dd11fe74c5def3411ab0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 16 Mar 2018 21:02:11 +0300 Subject: [PATCH 587/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6aa68af3..e94593d3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,6 +10,7 @@ PUT THE OUTPUT HERE ### Cloud Provider (where do you deploy Algo to) ``` PUT THE OUTPUT HERE @@ -19,6 +20,7 @@ PUT THE OUTPUT HERE + ### Steps to reproduce the behavior From 62fc22ab59f780db36137f3d615e65f4b8290e99 Mon Sep 17 00:00:00 2001 From: Damian Gerow Date: Fri, 16 Mar 2018 16:38:53 -0400 Subject: [PATCH 588/769] Creates a Docker container to run algo (#331) * Creates a Docker container to run algo * Simplistic testing of the Docker image This simply uses the same LXC system that was just tested. It's functional, but minimal. * More thorough tests against Docker This doubles the number of LXC containers in use, but does provide a more thorough test of the Docker image. --- .dockerignore | 13 ++++++++ .travis.yml | 10 ++++++- Dockerfile | 36 ++++++++++++++++++++++ algo-docker.sh | 44 +++++++++++++++++++++++++++ docs/Docker.md | 69 +++++++++++++++++++++++++++++++++++++++++++ tests/local-deploy.sh | 12 ++++++++ tests/update-users.sh | 8 ++++- 7 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 algo-docker.sh create mode 100644 docs/Docker.md create mode 100755 tests/local-deploy.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..40079d03 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.dockerignore +.git +.github +.gitignore +.travis.yml +CONTRIBUTING.md +Dockerfile +README.md +config.cfg +configs +docs +logo.png +tests diff --git a/.travis.yml b/.travis.yml index ae0adc43..5017a245 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ python: "2.7" sudo: required dist: trusty +services: + - docker + matrix: fast_finish: true @@ -32,6 +35,10 @@ before_cache: env: - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial - LXC_NAME=ubuntu1710 LXC_DISTRO=ubuntu LXC_RELEASE=artful + - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=artful + +before_install: + - test "${LXC_NAME}" != "docker" || docker build -t travis/algo . install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." @@ -41,6 +48,7 @@ install: - export LXC_IP="$(sudo lxc-info -Hin $LXC_NAME)" - sudo /bin/bash -c "printf '\n$LXC_IP test.lxc\n' >> /etc/hosts" - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' + - chmod 0644 ~/.ssh/config - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt @@ -52,7 +60,7 @@ script: # - shellcheck algo # - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check - - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" + - ./tests/local-deploy.sh after_script: - ./tests/update-users.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c2476ae1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,36 @@ +FROM python:2-alpine + +ARG VERSION="git" +ARG PACKAGES="bash libffi openssh-client openssl rsync tini" +ARG BUILD_PACKAGES="gcc libffi-dev linux-headers make musl-dev openssl-dev" + +LABEL name="algo" \ + version="${VERSION}" \ + description="Set up a personal IPsec VPN in the cloud" \ + maintainer="Trail of Bits " + +RUN apk --no-cache add ${PACKAGES} +RUN adduser -D -H -u 19857 algo +RUN mkdir -p /algo && mkdir -p /algo/configs + +WORKDIR /algo +COPY requirements.txt . +RUN apk --no-cache add ${BUILD_PACKAGES} && \ + python -m pip --no-cache-dir install -U pip && \ + python -m pip --no-cache-dir install virtualenv && \ + python -m virtualenv env && \ + source env/bin/activate && \ + python -m pip --no-cache-dir install -r requirements.txt && \ + apk del ${BUILD_PACKAGES} +COPY . . +RUN chmod 0755 /algo/algo-docker.sh + +# Because of the bind mounting of `configs/`, we need to run as the `root` user +# This may break in cases where user namespacing is enabled, so hopefully Docker +# sorts out a way to set permissions on bind-mounted volumes (`docker run -v`) +# before userns becomes default +# Note that not running as root will break if we don't have a matching userid +# in the container. The filesystem has also been set up to assume root. +USER root +CMD [ "/algo/algo-docker.sh" ] +ENTRYPOINT [ "/sbin/tini", "--" ] diff --git a/algo-docker.sh b/algo-docker.sh new file mode 100644 index 00000000..da458034 --- /dev/null +++ b/algo-docker.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -eEo pipefail + +ALGO_DIR="/algo" +DATA_DIR="/data" + +umask 0077 + +usage() { + retcode="${1:-0}" + echo "To run algo from Docker:" + echo "" + echo "docker run --cap-drop ALL -it -v :"${DATA_DIR}" trailofbits/algo:latest" + echo "" + exit ${retcode} +} + +if [ ! -f "${DATA_DIR}"/config.cfg ] ; then + echo "Looks like you're not bind-mounting your config.cfg into this container." + echo "algo needs a configuration file to run." + echo "" + usage -1 +fi + +if [ ! -e /dev/console ] ; then + echo "Looks like you're trying to run this container without a TTY." + echo "If you don't pass `-t`, you can't interact with the algo script." + echo "" + usage -1 +fi + +# To work around problems with bind-mounting Windows volumes, we need to +# copy files out of ${DATA_DIR}, ensure appropriate line endings and permissions, +# then copy the algo-generated files into ${DATA_DIR}. + +tr -d '\r' < "${DATA_DIR}"/config.cfg > "${ALGO_DIR}"/config.cfg +test -d "${DATA_DIR}"/configs && rsync -qLktr --delete "${DATA_DIR}"/configs "${ALGO_DIR}"/ + +"${ALGO_DIR}"/algo ${ALGO_ARGS} +retcode=${?} + +rsync -qLktr --delete "${ALGO_DIR}"/configs "${DATA_DIR}"/ +exit ${retcode} diff --git a/docs/Docker.md b/docs/Docker.md new file mode 100644 index 00000000..fba31193 --- /dev/null +++ b/docs/Docker.md @@ -0,0 +1,69 @@ +# Docker Support + +While it is not possible to run your Algo server from within a Docker container, it is possible to use Docker to provision your Algo server. + +## Limitations + +1. [Advanced](ADVANCED.md) installations are not currently supported; you must use the interactive `algo` script. +2. This has not yet been tested with user namespacing enabled. +3. If you're running this on Windows, take care when editing files under `configs/` to ensure that line endings are set appropriately for Unix systems. + +## Deploying an Algo Server with Docker + +1. Install [Docker](https://www.docker.com/community-edition#/download) -- setup and configuration is not covered here +2. Create a local directory to hold your VPN configs (e.g. `C:\Users\trailofbits\Documents\VPNs\`) +3. Create a local copy of [config.cfg](https://github.com/trailofbits/algo/blob/master/config.cfg), with required modifications (e.g. `C:\Users\trailofbits\Documents\VPNs\config.cfg`) +4. Run the Docker container, mounting your configurations appropriately: + - From Windows: + ```powershell + C:\Users\trailofbits> docker run --cap-drop=all -it \ + -v C:\Users\trailofbits\Documents\VPNs:/data \ + trailofbits/algo:latest + ``` + - From Linux: + ```bash + $ docker run --cap-drop-all -it \ + -v /home/trailofbits/Documents/VPNs:/data \ + trailofbits/algo:latest + ``` +5. When it exits, you'll be left with a fully populated `configs` directory, containing all appropriate configuration data for your clients, and for future server management + +### Providing Additional Files +f +If you need to provide additional files -- like authorization files for Google Cloud Project -- you can simply specify an additional `-v` parameter, and provide the appropriate path when prompted by `algo`. + +For example, you can specify `-v C:\Users\trailofbits\Documents\VPNs\gce_auth.json:/algo/gce_auth.json`, making the local path to your credentials JSON file `/algo/gce_auth.json`. + +## Managing an Algo Server with Docker + +Even though the container itself is transient, because you've persisted the configuration data, you can use the same Docker image to manage your Algo server. This is done by setting the environment variable `ALGO_ARGS`. + +If you want to use Algo to update the users on an existing server, specify `-e "ALGO_ARGS=update-users"` in your `docker run` command: +```powershell +$ docker run --cap-drop=all -it \ + -e "ALGO_ARGS=update-users" \ + -v C:\Users\trailofbits\Documents\VPNs:/data \ + trailofbits/algo:latest +``` + +## Building Your Own Docker Image + +You can use the Dockerfile provided in this repository as-is, or modify it to suit your needs. Further instructions on building an image can be found in the [Docker engine](https://docs.docker.com/engine/) documents. + +## Security Considerations + +Using Docker is largely no different from running Algo yourself, with a couple of notable exceptions: we run as root within the container, and you're retrieving your content from Docker Hub. + +To work around the limitations of bind mounts in docker, we have to run as root within the container. To mitigate concerns around doing this, we pass the `--cap-drop=all` parameter to `docker run`, which effectively removes all privileges from the root account, reducing it to a generic user account that happens to have a userid of 0. Further steps can be taken by applying `seccomp` profiles to the container; this is being considered as a future improvement. + +Docker themselves provide a concept of [Content Trust](https://docs.docker.com/engine/security/trust/content_trust/) for image management, which helps to ensure that the image you download is, in fact, the image that was uploaded. Content trust is still under development, and while we may be using it, its implementation, limitations, and constraints are documented with Docker. + +## Future Improvements + +1. Even though we're taking care to drop all capabilities to minimize the impact of running as root, we can probably include not only a `seccomp` profile, but also AppArmor and/or SELinux profiles as well. +2. The Docker image doesn't natively support [advanced](ADVANCED.md) Algo deployments, which is useful for scripting. This can be done by launching an interactive shell and running the commands yourself. +3. The way configuration is passed into and out of the container is a bit kludgy. Hopefully future improvements in Docker volumes will make this a bit easier to handle. + +## Advanced Usage + +If you want to poke around the Docker container yourself, you can do so by changing your `entrypoint`. Pass `--entrypoint=/bin/ash` as a parameter to `docker run`, and you'll be dropped into a full Linux shell in the container. diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh new file mode 100755 index 00000000..ddd58e92 --- /dev/null +++ b/tests/local-deploy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +DEPLOY_ARGS="server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" + +if [ "${LXC_NAME}" == "docker" ] +then + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e \"${DEPLOY_ARGS}\"" +else + ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "${DEPLOY_ARGS}" +fi diff --git a/tests/update-users.sh b/tests/update-users.sh index 8777c82a..df7066d1 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -3,10 +3,16 @@ set -ex CAPW=`cat /tmp/ca_password` +USER_ARGS="server_ip=$LXC_IP server_user=root ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" sed -i 's/- jack$/- jack_test/' config.cfg -ansible-playbook users.yml -e "server_ip=$LXC_IP server_user=root ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" +if [ "${LXC_NAME}" == "docker" ] +then + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\"" +else + ansible-playbook users.yml -e "${USER_ARGS}" +fi cd configs/$LXC_IP/pki/ From 4e4440a31834a915b7e32dcbf8e93ccc19ed54b5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 18 Mar 2018 00:16:22 +0300 Subject: [PATCH 589/769] Exclude CA from P12 (#835) --- roles/vpn/tasks/openssl.yml | 1 - roles/vpn/templates/client_windows.ps1.j2 | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 1c3e61bf..2457ea78 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -117,7 +117,6 @@ -export -name {{ item }} -out private/{{ item }}.p12 - -certfile cacert.pem -passout pass:"{{ easyrsa_p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index b984ab10..f5ef88b7 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,6 +1,7 @@ function AddAlgoVPN { certutil -f -importpfx .\{{ item }}.p12 + certutil -addstore root .\cacert.pem Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -Force } From 78830d96aa827ebe66b071a1bcfd6e5232386a67 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 19 Mar 2018 19:05:30 +0300 Subject: [PATCH 590/769] Android: add the CA and set the ciphers explicitly (#837) --- roles/vpn/templates/sswan.j2 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/roles/vpn/templates/sswan.j2 b/roles/vpn/templates/sswan.j2 index 4fa4fb84..405d44a2 100644 --- a/roles/vpn/templates/sswan.j2 +++ b/roles/vpn/templates/sswan.j2 @@ -3,10 +3,13 @@ "name": "Algo {{ IP_subject_alt_name }}", "type": "ikev2-cert", "remote": { - "addr": "{{ IP_subject_alt_name }}" + "addr": "{{ IP_subject_alt_name }}", + "cert": "{{ PayloadContentCA }}" }, "local": { "p12": "{{ item.1.stdout }}" }, + "ike-proposal": "{{ ciphers.defaults.ike | replace('!', '') }}", + "esp-proposal": "{{ ciphers.defaults.esp | replace('!', '') }}", "mtu": 1280 } From 1edb95df9c8ed7136726af4345acee7411d551bb Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 22 Mar 2018 13:26:50 +0000 Subject: [PATCH 591/769] Update client-android.md (#842) * Update client-android.md Changed Installation via profiles sections - Opening the helper html file in Chrome (v65.0.3325.109 on Android 6.0.1) does not work correctly. * Update client-android.md * Update client-android.md --- docs/client-android.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/client-android.md b/docs/client-android.md index 91c85fcc..1175da79 100644 --- a/docs/client-android.md +++ b/docs/client-android.md @@ -4,8 +4,8 @@ 1. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android). 2. Copy `android_{username}.sswan` and `android_{username}_helper.html` to your phone's internal storage. -3. Open the helper file in a browser (e.g., Google Chrome). -4. Click on the link. It opens the StrongSwan app and configures the VPN with your profile. +3. Open the StrongSwan app and go to 'Import VPN profile'. +4. Select the `android_{username}.sswan` file to configure the VPN with your profile. ## Manual installation From 51209a09949fc3ecf7b983bc2a348cdad0836a9c Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 27 Mar 2018 19:04:42 +0300 Subject: [PATCH 592/769] More debug for travis-ci --- .gitignore | 2 +- tests/local-deploy.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b68ae839..b632022a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ configs/* inventory_users *.kate-swp env -.DS_Store \ No newline at end of file +.DS_Store diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index ddd58e92..7779aef8 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -ex DEPLOY_ARGS="server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" @@ -8,5 +8,5 @@ if [ "${LXC_NAME}" == "docker" ] then docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e \"${DEPLOY_ARGS}\"" else - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "${DEPLOY_ARGS}" + ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "${DEPLOY_ARGS}" -vvvv fi From c378eacc00de858d9b10fdbccb02663d34cf0d6b Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 27 Mar 2018 19:10:59 +0300 Subject: [PATCH 593/769] Warn about local installation --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index dca852c3..eeeec046 100755 --- a/algo +++ b/algo @@ -542,7 +542,7 @@ algo_provisioning () { 5. Google Compute Engine 6. Scaleway 7. OpenStack (DreamCompute optimised) - 8. Install to existing Ubuntu 16.04 server + 8. Install to existing Ubuntu 16.04 server (Advanced) Enter the number of your desired provider : " From bb094a7b1653831a0d3f37ed67913a6e74fe5b48 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 27 Mar 2018 19:28:48 +0300 Subject: [PATCH 594/769] More debug for travis --- .travis.yml | 3 +++ playbooks/local.yml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5017a245..32daaaf4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ addons: - expect-dev - debootstrap - shellcheck + - tree cache: directories: @@ -54,6 +55,8 @@ install: - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt - pip install ansible-lint - gem install awesome_bot + - ansible-playbook --version + - tree . -L 2 script: # - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new diff --git a/playbooks/local.yml b/playbooks/local.yml index be2ecc9f..98a15774 100644 --- a/playbooks/local.yml +++ b/playbooks/local.yml @@ -23,7 +23,7 @@ blockinfile: dest: configs/inventory.dynamic marker: "# {mark} ALGO MANAGED BLOCK" - create: yes + create: true block: | [algo:children] {% for group in cloud_providers.keys() %} From ac8b092ca52e5adb6ead4f739d99ae83745b0994 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Tue, 27 Mar 2018 19:46:10 +0300 Subject: [PATCH 595/769] TravisCI tests --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 32daaaf4..6971d1ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,8 @@ install: - chmod 0644 ~/.ssh/config - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - - sudo apt-get install build-essential libssl-dev libffi-dev python-dev && sudo pip install -r requirements.txt + - sudo apt-get install build-essential libssl-dev libffi-dev python-dev + - pip install -r requirements.txt - pip install ansible-lint - gem install awesome_bot - ansible-playbook --version From 32cbec6f5b5a279c6e9d6a8dcd8084f0b088bdd3 Mon Sep 17 00:00:00 2001 From: Utkan Gezer Date: Tue, 27 Mar 2018 21:50:50 +0300 Subject: [PATCH 596/769] Multi-line virtualenv setup script (#829) Changed the single-line virtualenv setup script into multi-line one. Should be equivalent to what it was before, and now viewable/copy-able without scrolling. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a740731f..63a7f7b9 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,10 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 4. **Install Algo's remaining dependencies.** Use the same Terminal window as the previous step and run: ```bash - $ python -m virtualenv --python=`which python2` env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt + $ python -m virtualenv --python=`which python2` env && + source env/bin/activate && + python -m pip install -U pip && + python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. From aea9c9a5e245cfe4441d71f4121251f32d25024c Mon Sep 17 00:00:00 2001 From: Arun John Kuruvilla Date: Tue, 27 Mar 2018 14:53:13 -0400 Subject: [PATCH 597/769] Removed ssh_public_key variable for AWS. Issue #773 (#817) --- algo | 2 +- docs/deploy-from-ansible.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/algo b/algo index eeeec046..403608a8 100755 --- a/algo +++ b/algo @@ -281,7 +281,7 @@ Enter the number of your desired region: esac ROLES="ec2 vpn cloud" - EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name ssh_public_key=$ssh_public_key region=$region" + EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name region=$region" } lightsail () { diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 646d838a..e6fb2b05 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -87,7 +87,6 @@ Required variables: - aws_access_key - aws_secret_key - aws_server_name -- ssh_public_key - region Possible options for `region`: From 4b0aea8f5a7a6226d944db1601bc665ac94ade6d Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Wed, 28 Mar 2018 13:17:56 -0500 Subject: [PATCH 598/769] Document iptables rules (#854) * Remove firewall rule related to the old proxy role * Remove proxy conditionals from mobileconfig template * Add comments explaining firewall rules --- roles/vpn/tasks/client_configs.yml | 1 - roles/vpn/templates/mobileconfig.j2 | 20 ----------- roles/vpn/templates/rules.v4.j2 | 54 ++++++++++++++++++++++++++++- roles/vpn/templates/rules.v6.j2 | 52 +++++++++++++++++++++++++-- 4 files changed, 102 insertions(+), 25 deletions(-) diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index ea1621a2..5c32ded7 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -9,7 +9,6 @@ - name: Set facts for mobileconfigs set_fact: - proxy_enabled: false PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - name: Build the mobileconfigs diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index ce51ea5a..b8013df2 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -124,24 +124,12 @@ Proxies HTTPEnable -{% if proxy_enabled is defined and proxy_enabled == true %} - 1 - HTTPPort - 8118 - HTTPProxy - {{ local_service_ip }} - {% else %} 0 -{% endif %} HTTPSEnable 0 UserDefinedName -{% if proxy_enabled is defined and proxy_enabled == true %} - Algo VPN {{ IP_subject_alt_name }} IKEv2 with proxy - {% else %} Algo VPN {{ IP_subject_alt_name }} IKEv2 -{% endif %} VPNType IKEv2 @@ -187,17 +175,9 @@ PayloadDisplayName -{% if proxy_enabled is defined and proxy_enabled == true %} - {{ IP_subject_alt_name }} IKEv2 with proxy - {% else %} {{ IP_subject_alt_name }} IKEv2 -{% endif %} PayloadIdentifier -{% if proxy_enabled is defined and proxy_enabled == true %} - donut.local.{{ 600000 | random | to_uuid | upper }} - {% else %} donut.local.{{ 500000 | random | to_uuid | upper }} -{% endif %} PayloadRemovalDisallowed PayloadType diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index e040b184..c51568aa 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -1,43 +1,95 @@ +#### The mangle table +# This table allows us to modify packet headers +# Packets enter this table first +# *mangle + :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + {% if max_mss is defined %} +# MSS is the TCP Max Segment Size +# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation +# This appears to be necessary on (at least) Google Cloud, +# however, some routers also require a change to this parameter +# See also: +# - https://github.com/trailofbits/algo/issues/216 +# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu +# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan -A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} + COMMIT + + +#### The nat table +# This table enables Network Address Translation +# (This is technically a type of packet mangling) +# *nat + :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + +# Allow traffic from the VPN network to the outside world, and replies -A POSTROUTING -s {{ vpn_network }} -m policy --pol none --dir out -j MASQUERADE + COMMIT + + +#### The filter table +# The default ipfilter table +# *filter + +# By default, drop packets that are destined for this server :INPUT DROP [0:0] +# By default, drop packets that request to be forwarded by this server :FORWARD DROP [0:0] +# By default, accept any packets originating from this server :OUTPUT ACCEPT [0:0] + +# Accept packets destined for localhost -A INPUT -i lo -j ACCEPT +# Accept any packet from an open TCP connection -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Accept packets using the encapsulation protocol -A INPUT -p esp -j ACCEPT -A INPUT -p ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT +# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +# Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT +# Allow any traffic from the VPN -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT + # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. + +# Accept DNS traffic to the local DNS resolver -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT --A INPUT -d {{ local_service_ip }} -p tcp -m multiport --dport 8080,8118 -j ACCEPT + {% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +# Drop traffic between VPN clients -A FORWARD -s {{ vpn_network }} -d {{ vpn_network }} -j DROP {% endif %} + +# Forward any packet that's part of an established connection -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Drop SMB/CIFS traffic that requests to be forwarded -A FORWARD -p tcp --dport 445 -j DROP +# Drop NETBIOS trafic that requests to be forwarded -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP + +# Forward any IPSEC traffic from the VPN network -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT + COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 717b887f..82ca8e16 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -1,43 +1,89 @@ +#### The mangle table +# This table allows us to modify packet headers +# Packets enter this table first +# *mangle + :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + {% if max_mss is defined %} +# MSS is the TCP Max Segment Size +# See rules.v4 for a more complete explanation -A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} + COMMIT + +#### The nat table +# This table enables Network Address Translation +# (This is technically a type of packet mangling) +# *nat + :PREROUTING ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] + +# Allow traffic from the VPN network to the outside world, and replies -A POSTROUTING -s {{ vpn_network_ipv6 }} -m policy --pol none --dir out -j MASQUERADE + COMMIT + +#### The filter table +# The default ipfilter table +# *filter + +# By default, drop packets that are destined for this server :INPUT DROP [0:0] +# By default, drop packets that request to be forwarded by this server :FORWARD DROP [0:0] +# By default, accept any packets originating from this server :OUTPUT ACCEPT [0:0] + +# Create the ICMPV6-CHECK chain and its log chain +# These chains are used later to prevent a type of bug that would +# allow malicious traffic to reach over the server into the private network +# An instance of such a bug on Cisco software is described here: +# https://www.insinuator.net/2016/05/cve-2016-1409-ipv6-ndp-dos-vulnerability-in-cisco-software/ +# other software implementations might be at least as broken as the one in CISCO gear. :ICMPV6-CHECK - [0:0] :ICMPV6-CHECK-LOG - [0:0] + +# Accept packets destined for localhost -A INPUT -i lo -j ACCEPT +# Accept any packet from an open TCP connection -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT +# Accept packets using the encapsulation protocol -A INPUT -p esp -j ACCEPT -A INPUT -m ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT +# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) -A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +# Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT + +# Accept properly formatted Neighbor Discovery Protocol packets -A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT -A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT + # DHCP in AWS -A INPUT -m conntrack --ctstate NEW -m udp -p udp --dport 546 -d fe80::/64 -j ACCEPT + # TODO: # The IP of the resolver should be bound to a DUMMY interface. # DUMMY interfaces are the proper way to install IPs without assigning them any # particular virtual (tun,tap,...) or physical (ethernet) interface. + +# Accept DNS traffic to the local DNS resolver -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT + {% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} -A FORWARD -s {{ vpn_network_ipv6 }} -d {{ vpn_network_ipv6 }} -j DROP {% endif %} @@ -47,13 +93,13 @@ COMMIT -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT -# this is so potential malicious traffic can not reach anywhere over the server -# https://www.insinuator.net/2016/05/cve-2016-1409-ipv6-ndp-dos-vulnerability-in-cisco-software/ -# other software implementations might be at least as broken as the one in CISCO gear. + +# Use the ICMPV6-CHECK chain, described above -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-advertisement -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-solicitation -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type neighbor-advertisement -j ICMPV6-CHECK-LOG -A ICMPV6-CHECK-LOG -j LOG --log-prefix "ICMPV6-CHECK-LOG DROP " -A ICMPV6-CHECK-LOG -j DROP + COMMIT From a8784bc0f4c14c6de6f4b7d91b467b5a3a3818c6 Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Wed, 28 Mar 2018 13:20:17 -0500 Subject: [PATCH 599/769] Add FAQ entry regarding IPSEC backdoor (#460) (#853) --- docs/faq.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 362a0d20..65d2f9ed 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,6 +8,7 @@ * [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) * [I deployed an Algo server. Can you update it with new features?](#i-deployed-an-algo-server-can-you-update-it-with-new-features) * [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) +* [Wasn't IPSEC backdoored by the US government?](#wasnt-ipsec-backdoored-by-the-us-government) ## Has Algo been audited? @@ -44,3 +45,23 @@ In the future, we will make it easier for users who want to update their own ser ## Where did the name "Algo" come from? Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). + +## Wasn't IPSEC backdoored by the US government? + +No. + +[Per security researcher Thomas Ptacek](https://news.ycombinator.com/item?id=2014197): + +> In 2001, Angelos Keromytis --- then a grad student at Penn, now a Columbia professor --- added support for hardware-accelerated IPSEC NICs. When you have an IPSEC NIC, the channel between the NIC and the IPSEC stack keeps state to tell the stack not to bother doing the things the NIC already did, among them validating the IPSEC ESP authenticator. Angelos' code had a bug; it appears to have done the software check only when the hardware had already done it, and skipped it otherwise. +> +> The bug happened during a change that simultaneously refactored and added a feature to OpenBSD's ESP code; a comparison that should have been == was instead !=; the "if" statement with the bug was originally and correctly !=, but should have been flipped based on how the code was refactored. +> +> HD Moore may as we speak be going through the pain of reconstituting a nearly decade-old version of OpenBSD to verify the bug, but stipulate that it was there, and here's what you get: IPSEC ESP packet authentication was disabled if you didn't have hardware IPSEC. There is probably an elaborate man-in-the-middle scenario in which this could get you traffic inspection, but it's nowhere nearly as straightforward as leaking key bits. +> +> To entertain the conspiracy theory, you're still suggesting that the FBI not only introduced this bug, but also developed the technology required to MITM ESP sessions, bouncing them through some secret FBI-developed middlebox. +> +> One year later, Jason Wright from NETSEC (the company at the heart of the [I think silly] allegations about OpenBSD IPSEC backdoors) fixed the bug. +> +> It's interesting that the bug was fixed without an advisory (oh to be a fly on the wall on ICB that day; Theo had a, um, a, "way" with his dev team). On the other hand, we don't know what releases of OpenBSD actually had the bug right now. +> +> It seems vanishingly unlikely that there could have been anything deliberate about this series of changes. You are unlikely to find anyone who will impugn Angelos. Meanwhile, the diffs tell exactly the opposite of the story that Greg Perry told. \ No newline at end of file From e944ee993a91b143f7592183df910f85aa0309a5 Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Wed, 28 Mar 2018 13:20:43 -0500 Subject: [PATCH 600/769] Embed certs into Windows deployment scripts (#840) - Obviate need to copy separate script and certificate files - Allow execution from any directory, not just the script's parent directory (no assumption of any particular working directory) - Fix docs that neglected to mention copying cacert.pem - Fix docs that incorrectly referred to the user cert store As part of this work, rewrite the windows_client.ps1.j2 deployment script template - Add comment-based help - Require admin privileges - Use a Param() block - Use parameter sets with -Add and -Remove switches - Add the -GetInstalledCerts switch, to list any Algo certificates installed the machine's cert store - Add the -SaveCerts switch, to save the embedded certificates to files - Put Jinja2 variables inside Powershell variables, - Use native Powershell cmdlets rather than shell out to certutil.exe - Add a playbook to regenerate the windows_USER.ps1 scripts --- README.md | 4 +- docs/client-windows.md | 68 +++++-- playbooks/win_script_rebuild.yml | 67 +++++++ roles/vpn/tasks/client_configs.yml | 6 +- roles/vpn/templates/client_windows.ps1.j2 | 205 ++++++++++++++++++++-- 5 files changed, 320 insertions(+), 30 deletions(-) create mode 100644 playbooks/win_script_rebuild.yml diff --git a/README.md b/README.md index 63a7f7b9..e416338e 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,9 @@ No version of Android supports IKEv2. Install the [strongSwan VPN Client for And ### Windows 10 -Copy your PowerShell script `windows_{username}.ps1` and p12 certificate `{username}.p12` to the Windows client and run the following command as Administrator to configure the VPN connection. +Copy your PowerShell script `windows_{username}.ps1` to the Windows client and run the following command as Administrator to configure the VPN connection. ``` -powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 Add +powershell -ExecutionPolicy ByPass -File windows_{username}.ps1 -Add ``` For a manual installation, see the [Windows setup instructions](/docs/client-windows.md). diff --git a/docs/client-windows.md b/docs/client-windows.md index 1013585c..d7d89151 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -1,27 +1,69 @@ # Windows client manual setup -Windows clients have a more complicated setup than most others. Follow the steps below to set one up: +## Automatic installtion -1. Copy the CA certificate (`cacert.pem`), user certificate (`$user.p12`), and the user PowerShell script (`windows_$user.ps1`) to the client computer. -2. Import the CA certificate to the local machine Trusted Root certificate store. -3. Open PowerShell as Administrator. Navigate to your copied files. -4. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. +To install automatically, use the generated user Powershell script. + +1. Copy the user PowerShell script (`windows_USER.ps1`) to the client computer. +2. Open Powershell as Administrator. +3. Run the following command: +```powershell +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add +``` +4. The command has help information available. To view its full help, run this from Powershell: +```powershell +Get-Help -Name .\windows_USER.ps1 -Full | more +``` + +## Manual installation + +1. Copy the CA certificate (`cacert.pem`) and user certificate (`USER.p12`) to the client computer +2. Open PowerShell as Administrator. Navigate to your copied files. +3. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. ```powershell Set-ExecutionPolicy Unrestricted -Scope CurrentUser ``` -5. In the same PowerShell window, run the included PowerShell script to import the user certificate, set up a VPN connection, and activate stronger ciphers on it. -6. After you execute the user script, set the Execution Policy back before you close the PowerShell window. +4. In the same window, run the necessary commands to install the certificates and create the VPN configuration. Note the lines at the top defining the VPN address, USER.p12 file location, and CA certificate location - change those lines to the IP address of your Algo server and the location you saved those two files. Also note that it will prompt for the "User p12 password", which is printed at the end of a successful Algo deployment. + +```powershell +$VpnServerAddress = "1.2.3.4" +$UserP12Path = "$Home\Downloads\USER.p12" +$CaCertPath = "$Home\Downloads\cacert.pem" +$VpnName = "Algo VPN $VpnServerAddress IKEv2" +$p12Pass = Read-Host -AsSecureString -Prompt "User p12 password" + +Import-PfxCertificate -FilePath $UserP12Path -CertStoreLocation Cert:\LocalMachine\My -Password $p12Pass +Import-Certificate -FilePath $CaCertPath -CertStoreLocation Cert:\LocalMachine\Root + +$addVpnParams = @{ + Name = $VpnName + ServerAddress = $VpnServerAddress + TunnelType = "IKEv2" + AuthenticationMethod = "MachineCertificate" + EncryptionLevel = "Required" +} +Add-VpnConnection @addVpnParams + +$setVpnParams = @{ + ConnectionName = $VpnName + AuthenticationTransformConstants = "GCMAES128" + CipherTransformConstants = "GCMAES128" + EncryptionMethod = "AES128" + IntegrityCheckMethod = "SHA384" + DHGroup = "ECP256" + PfsGroup = "ECP256" + Force = $true +} +Set-VpnConnectionIPsecConfiguration @setVpnParams + +``` + +5. After you execute the user script, set the Execution Policy back before you close the PowerShell window. ```powershell Set-ExecutionPolicy Restricted -Scope CurrentUser ``` Your VPN is now installed and ready to use. - -If you want to perform these steps by hand, you will need to import the user certificate to the Personal certificate store, add an IKEv2 connection in the network settings, then activate stronger ciphers on it via the following PowerShell script: - -```powershell -Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -``` diff --git a/playbooks/win_script_rebuild.yml b/playbooks/win_script_rebuild.yml new file mode 100644 index 00000000..12bdfe91 --- /dev/null +++ b/playbooks/win_script_rebuild.yml @@ -0,0 +1,67 @@ +--- + +# This playbook is designed to help when modifying the Windows script template +# in roles/vpn/templates/client_windows.ps1.j2 +# It rebuilds the client_USER.ps1 scripts for each user defined in config.cfg, +# without redeploying users or opening an SSH connection to the Algo server at +# all. +# +# This playbook is _not_ part of a normal Algo deployment. +# It is only intended to speed up development of the client_USER.ps1 Windows +# Algo install scripts. +# +# REQUIREMENTS +# - Algo must have been deployed once +# - Windows users must have been enabled at deployment time +# - All users defined in config.cfg must not have changed +# - Only one Algo deployment exists in the configs/ directory +# - There must be exactly one subfolder in the configs/ directory: +# the folder named after the IP of the algo server + +- hosts: localhost + gather_facts: False + tags: always + vars_files: + - ../config.cfg + + tasks: + + - name: Get config subdir + shell: find ../configs/* -maxdepth 0 -type d | sed 's/.*\///' + register: config_subdir_result + - fail: + msg: + - "Found wrong number of config subdirs... stdout:" + - "{{ config_subdir_result.split('\n') }}" + when: config_subdir_result.stdout.split('\n') | length != 1 + - set_fact: + IP_subject_alt_name: "{{ config_subdir_result.stdout }}" + - debug: + var: IP_subject_alt_name + + - name: Register p12 PayloadContent + shell: cat private/{{ item }}.p12 | base64 + register: PayloadContent + args: + chdir: "../configs/{{ IP_subject_alt_name }}/pki/" + with_items: "{{ users }}" + + - name: Set facts for mobileconfigs + set_fact: + proxy_enabled: false + PayloadContentCA: "{{ lookup('file' , '../configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + + - name: Build the windows client powershell script + template: + src: ../roles/vpn/templates/client_windows.ps1.j2 + dest: ../configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 + mode: 0600 + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" + + - name: List windows client powershell scripts + debug: + msg: "configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1" + with_items: + - "{{ users }}" diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index 5c32ded7..4c6cbe92 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -70,10 +70,12 @@ - name: Build the windows client powershell script template: src: client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1 + dest: configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 mode: 0600 when: Win10_Enabled is defined and Win10_Enabled == "Y" or supports_windows.stat.exists == true - with_items: "{{ users }}" + with_together: + - "{{ users }}" + - "{{ PayloadContent.results }}" - name: Restrict permissions for the local private directories file: diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index f5ef88b7..93269c7f 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -1,19 +1,198 @@ +#Requires -RunAsAdministrator -function AddAlgoVPN { - certutil -f -importpfx .\{{ item }}.p12 - certutil -addstore root .\cacert.pem - Add-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -ServerAddress "{{ IP_subject_alt_name }}" -TunnelType IKEv2 -AuthenticationMethod MachineCertificate -EncryptionLevel Required - Set-VpnConnectionIPsecConfiguration -ConnectionName "Algo VPN {{ IP_subject_alt_name }} IKEv2" -AuthenticationTransformConstants GCMAES128 -CipherTransformConstants GCMAES128 -EncryptionMethod AES128 -IntegrityCheckMethod SHA384 -DHGroup ECP256 -PfsGroup ECP256 -Force +<# +.SYNOPSIS +Add or remove the Algo VPN + +.DESCRIPTION +Add or remove the Algo VPN +See the examples for more information + +.PARAMETER Add +Add the VPN to the local system + +.PARAMETER Remove +Remove the VPN from the local system + +.PARAMETER GetInstalledCerts +Retrieve Algo certs, if any, from the system certificate store + +.PARAMETER SaveCerts +Save the Algo certs embedded in this file + +.PARAMETER OutputDirectory +When saving the Algo certs, save to this directory + +.PARAMETER Pkcs12DecryptionPassword +The decryption password for the user's PKCS12 certificate, sometimes called the "p12 password". +Note that this must be passed in as a SecureString, not a regular string. +You can create a secure string with the `Read-Host -AsSecureString` cmdlet. +See the examples for more information. + +.EXAMPLE +client_USER.ps1 -Add + +Adds the Algo VPN + +.EXAMPLE +$p12pass = Read-Host -AsSecureString; client_USER.ps1 -Add -Pkcs12DecryptionPassword $p12pass + +Create a variable containing the PKCS12 decryption password, then use it when adding the VPN. +This can be especially useful when troubleshooting, because you can use the same variable with +multiple calls to client_USER.ps1, rather than having to type the PKCS12 password each time. + +.EXAMPLE +client_USER.ps1 -Remove + +Removes the Algo VPN if installed. + +.EXAMPLE +client_USER.ps1 -GetIntalledCerts + +Show the Algo VPN's installed certificates, if any. + +.EXAMPLE +client_USER.ps1 -SaveCerts -OutputDirectory $Home\Downloads + +Save the embedded CA cert and encrypted user PKCS12 file. +#> +[CmdletBinding(DefaultParameterSetName="Add")] Param( + [Parameter(ParameterSetName="Add")] + [Switch] $Add, + + [Parameter(ParameterSetName="Add")] + [SecureString] $Pkcs12DecryptionPassword, + + [Parameter(Mandatory, ParameterSetName="Remove")] + [Switch] $Remove, + + [Parameter(Mandatory, ParameterSetName="GetInstalledCerts")] + [Switch] $GetInstalledCerts, + + [Parameter(Mandatory, ParameterSetName="SaveCerts")] + [Switch] $SaveCerts, + + [Parameter(ParameterSetName="SaveCerts")] + [string] $OutputDirectory = "$PWD" +) + +$ErrorActionPreference = "Stop" + +$VpnServerAddress = "{{ IP_subject_alt_name }}" +$VpnName = "Algo VPN {{ IP_subject_alt_name }} IKEv2" +$VpnUser = "{{ item.0 }}" +$CaCertificateBase64 = "{{ PayloadContentCA }}" +$UserPkcs12Base64 = "{{ item.1.stdout }}" + +if ($PsCmdlet.ParameterSetName -eq "Add" -and -not $Pkcs12DecryptionPassword) { + $Pkcs12DecryptionPassword = Read-Host -AsSecureString -Prompt "Pkcs12DecryptionPassword" } -function RemoveAlgoVPN { - Get-ChildItem cert:LocalMachine/Root | Where-Object { $_.Subject -match '^CN={{ IP_subject_alt_name }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item - Get-ChildItem cert:LocalMachine/My | Where-Object { $_.Subject -match '^CN={{ item }}$' -and $_.Issuer -match '^CN={{ IP_subject_alt_name }}$' } | Remove-Item - Remove-VpnConnection -name "Algo VPN {{ IP_subject_alt_name }} IKEv2" -Force +<# +.SYNOPSIS +Create a temporary directory +#> +function New-TemporaryDirectory { + [CmdletBinding()] Param() + do { + $guid = New-Guid | Select-Object -ExpandProperty Guid + $newTempDirPath = Join-Path -Path $env:TEMP -ChildPath $guid + } while (Test-Path -Path $newTempDirPath) + New-Item -ItemType Directory -Path $newTempDirPath } -switch ($args[0]) { - "Add" { AddAlgoVPN } - "Remove" { RemoveAlgoVPN } - default { Write-Host Usage: $MyInvocation.MyCommand.Name "(Add|Remove)" } +<# +.SYNOPSIS +Retrieve any installed Algo VPN certificates +#> +function Get-InstalledAlgoVpnCertificates { + [CmdletBinding()] Param() + Get-ChildItem -LiteralPath Cert:\LocalMachine\Root | + Where-Object { + $_.Subject -match "^CN=${VpnServerAddress}$" -and $_.Issuer -match "^CN=${VpnServerAddress}$" + } + Get-ChildItem -LiteralPath Cert:\LocalMachine\My | + Where-Object { + $_.Subject -match "^CN=${VpnUser}$" -and $_.Issuer -match "^CN=${VpnServerAddress}$" + } +} + +function Save-AlgoVpnCertificates { + [CmdletBinding()] Param( + [String] $OutputDirectory = $PWD + ) + $caCertPath = Join-Path -Path $OutputDirectory -ChildPath "cacert.pem" + $userP12Path = Join-Path -Path $OutputDirectory -ChildPath "$VpnUser.p12" + # NOTE: We cannot use ConvertFrom-Base64 here because it is not designed for binary data + [IO.File]::WriteAllBytes( + $caCertPath, + [Convert]::FromBase64String($CaCertificateBase64)) + [IO.File]::WriteAllBytes( + $userP12Path, + [Convert]::FromBase64String($UserPkcs12Base64)) + return New-Object -TypeName PSObject -Property @{ + CaPem = $caCertPath + UserPkcs12 = $userP12Path + } +} + +function Add-AlgoVPN { + [Cmdletbinding()] Param() + + $workDir = New-TemporaryDirectory + + try { + $certs = Save-AlgoVpnCertificates -OutputDirectory $workDir + $importPfxCertParams = @{ + Password = $Pkcs12DecryptionPassword + FilePath = $certs.UserPkcs12 + CertStoreLocation = "Cert:\LocalMachine\My" + } + Import-PfxCertificate @importPfxCertParams + $importCertParams = @{ + FilePath = $certs.CaPem + CertStoreLocation = "Cert:\LocalMachine\Root" + } + Import-Certificate @importCertParams + } finally { + Remove-Item -Recurse -Force -LiteralPath $workDir + } + + $addVpnParams = @{ + Name = $VpnName + ServerAddress = $VpnServerAddress + TunnelType = "IKEv2" + AuthenticationMethod = "MachineCertificate" + EncryptionLevel = "Required" + } + Add-VpnConnection @addVpnParams + + $setVpnParams = @{ + ConnectionName = $VpnName + AuthenticationTransformConstants = "GCMAES128" + CipherTransformConstants = "GCMAES128" + EncryptionMethod = "AES128" + IntegrityCheckMethod = "SHA384" + DHGroup = "ECP256" + PfsGroup = "ECP256" + Force = $true + } + Set-VpnConnectionIPsecConfiguration @setVpnParams +} + +function Remove-AlgoVPN { + [CmdletBinding()] Param() + Get-InstalledAlgoVpnCertificates | Remove-Item -Force + Remove-VpnConnection -Name $VpnName -Force +} + +switch ($PsCmdlet.ParameterSetName) { + "Add" { Add-AlgoVPN } + "Remove" { Remove-AlgoVPN } + "GetInstalledCerts" { Get-InstalledAlgoVpnCertificates } + "SaveCerts" { + $certs = Save-AlgoVpnCertificates -OutputDirectory $OutputDirectory + Get-Item -LiteralPath $certs.UserPkcs12, $certs.CaPem + } + default { throw "Unknown parameter set: '$($PsCmdlet.ParameterSetName)'" } } From a2e051ef00200e11d3f74cdfc9d4787f81e582c7 Mon Sep 17 00:00:00 2001 From: Micah R Ledbetter Date: Wed, 28 Mar 2018 13:24:20 -0500 Subject: [PATCH 601/769] Add a workaround for disabling DNS filtering to the FAQ (#852) * Add a workaround for disabling DNS filtering to the FAQ * Update faq.md --- docs/faq.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 65d2f9ed..b55a911e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,6 +8,7 @@ * [Why aren't you using Alpine Linux, OpenBSD, or HardenedBSD?](#why-arent-you-using-alpine-linux-openbsd-or-hardenedbsd) * [I deployed an Algo server. Can you update it with new features?](#i-deployed-an-algo-server-can-you-update-it-with-new-features) * [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) +* [Can DNS filtering be disabled?](#can-dns-filtering-be-disabled) * [Wasn't IPSEC backdoored by the US government?](#wasnt-ipsec-backdoored-by-the-us-government) ## Has Algo been audited? @@ -46,6 +47,10 @@ In the future, we will make it easier for users who want to update their own ser Algo is short for "Al Gore", the **V**ice **P**resident of **N**etworks everywhere for [inventing the Internet](https://www.youtube.com/watch?v=BnFJ8cHAlco). +## Can DNS filtering be disabled? + +There is no official way to disable DNS filtering, but there is a workaround: SSH to your Algo server (using the 'shell access' command printed upon a successful deployment), edit `/etc/ipsec.conf`, and change `rightdns=172.16.0.1` to `rightdns=8.8.8.8`. Then run `ipsec restart`. If all else fails, we recommend deploying a new Algo server without the adblocking feature enabled. + ## Wasn't IPSEC backdoored by the US government? No. @@ -64,4 +69,4 @@ No. > > It's interesting that the bug was fixed without an advisory (oh to be a fly on the wall on ICB that day; Theo had a, um, a, "way" with his dev team). On the other hand, we don't know what releases of OpenBSD actually had the bug right now. > -> It seems vanishingly unlikely that there could have been anything deliberate about this series of changes. You are unlikely to find anyone who will impugn Angelos. Meanwhile, the diffs tell exactly the opposite of the story that Greg Perry told. \ No newline at end of file +> It seems vanishingly unlikely that there could have been anything deliberate about this series of changes. You are unlikely to find anyone who will impugn Angelos. Meanwhile, the diffs tell exactly the opposite of the story that Greg Perry told. From 7c087aeed94011a2b9e7ada8ff7d07030b0ded7e Mon Sep 17 00:00:00 2001 From: Anton T Johansson Date: Thu, 29 Mar 2018 23:33:18 +0200 Subject: [PATCH 602/769] Fixed path in Network Manager section (#860) "configs" directory missing in paths. --- docs/client-linux.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/client-linux.md b/docs/client-linux.md index a5155501..954839cb 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -61,11 +61,11 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * Name: your choice, e.g.: *ikev2-1.2.3.4* * Gateway: * Address: IP of the Algo VPN server, e.g: `1.2.3.4` - * Certificate: `cacert.pem` found at `/path/to/algo/1.2.3.4/cacert.pem` + * Certificate: `cacert.pem` found at `/path/to/algo/configs/1.2.3.4/cacert.pem` * Client: * Authentication: *Certificate/Private key* - * Certificate: `user-name.crt` found at `/path/to/algo/1.2.3.4/pki/certs/user-name.crt` - * Private key: `user-name.key` found at `/path/to/algo/1.2.3.4/pki/private/user-name.key` + * Certificate: `user-name.crt` found at `/path/to/algo/configs/1.2.3.4/pki/certs/user-name.crt` + * Private key: `user-name.key` found at `/path/to/algo/configs/1.2.3.4/pki/private/user-name.key` * Options: * Check *Request an inner IP address*, connection will fail without this option * Optionally check *Enforce UDP encapsulation* @@ -75,4 +75,4 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * Check *Enable custom proposals* * IKE: `aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256` * ESP: `aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256` -* Apply and turn the connection on, you should now be connected \ No newline at end of file +* Apply and turn the connection on, you should now be connected From a8b4a47a884cc0c357205f80ae790d7613218a5d Mon Sep 17 00:00:00 2001 From: iliyan jeliazkov Date: Wed, 18 Apr 2018 21:10:03 -0500 Subject: [PATCH 603/769] Updating the language of the instructions (#880) --- docs/cloud-azure.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cloud-azure.md b/docs/cloud-azure.md index ae836815..261f4bcf 100644 --- a/docs/cloud-azure.md +++ b/docs/cloud-azure.md @@ -12,8 +12,8 @@ | 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | | 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | | 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | -| 11. Select a role (Contributor will enough for all)| [![step11-thumb]][step11-screen] | -| 12. Swith next to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | +| 11. Select a role (Contributor will be sufficient)| [![step11-thumb]][step11-screen] | +| 12. Next, switch to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | Now you can use Environment Variables: From e78df4046806ca597f646dc6fca7804f9f592248 Mon Sep 17 00:00:00 2001 From: Cat Jones <31020910+catherinejones@users.noreply.github.com> Date: Mon, 23 Apr 2018 18:58:40 -0400 Subject: [PATCH 604/769] adds DigitalOcean documentation (#869) --- README.md | 1 + docs/cloud-do.md | 87 ++++++++++++++++++++++++++++++++++ docs/images/do-api.png | Bin 0 -> 107121 bytes docs/images/do-new-token.png | Bin 0 -> 114968 bytes docs/images/do-view-token.png | Bin 0 -> 152628 bytes docs/index.md | 1 + 6 files changed, 89 insertions(+) create mode 100644 docs/cloud-do.md create mode 100644 docs/images/do-api.png create mode 100644 docs/images/do-new-token.png create mode 100644 docs/images/do-view-token.png diff --git a/README.md b/README.md index e416338e..54e72711 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ After this process completes, the Algo VPN server will contains only the users l - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](docs/cloud-azure.md) + - Configure [DigitalOcean](cloud-do.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server diff --git a/docs/cloud-do.md b/docs/cloud-do.md new file mode 100644 index 00000000..15c8e288 --- /dev/null +++ b/docs/cloud-do.md @@ -0,0 +1,87 @@ +# DigitalOcean cloud setup + +## API Token creation + +First, login into your DigitalOcean account. + +Select **API** from the titlebar. This will take you to the "Applications & API" page. + +![The Applications & API page](/docs/images/do-api.png) + +On the **Tokens/Keys** tab, select **Generate New Token**. A dialog will pop up. In that dialog, give your new token a name, and make sure **Write** is checked off. Click the **Generate Token** button when you are ready. + +![The new token dialog, showing a form requesting a name and confirmation on the scope for the new token.](/docs/images/do-new-token.png) + +You will be returned to the **Tokens/Keys** tab, and your new key will be shown under the **Personal Access Tokens** header. + +![The new token in the listing.](/docs/images/do-view-token.png) + +Copy or note down the hash that shows below the name you entered, as this will be necessary for the steps below. This value will disappear if you leave this page, and you'll need to regenerate it if you forget it. + +## Using DigitalOcean with Algo (command) + +These steps are for people who run Algo using Docker or using the "algo" command. + +First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. + +``` + What provider would you like to use? + 1. DigitalOcean + 2. Amazon Lightsail + 3. Amazon EC2 + 4. Microsoft Azure + 5. Google Compute Engine + 6. Scaleway + 7. OpenStack (DreamCompute optimised) + 8. Install to existing Ubuntu 16.04 server + +Enter the number of your desired provider +: 1 +``` + +Next you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (don't worry if don't see any output, as the key input is hidden by Algo). + +``` +Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): +[pasted values will not be displayed] +: +``` + +You will be prompted for the server name to enter. Feel free to leave this as the default ("algo.local") if you are not certain how this will affect your setup. + +``` +Name the vpn server: +[algo.local]: +``` + +After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. + +``` + What region should the server be located in? + 1. Amsterdam (Datacenter 2) + 2. Amsterdam (Datacenter 3) + 3. Frankfurt + 4. London + 5. New York (Datacenter 1) + 6. New York (Datacenter 2) + 7. New York (Datacenter 3) + 8. San Francisco (Datacenter 1) + 9. San Francisco (Datacenter 2) + 10. Singapore + 11. Toronto + 12. Bangalore +Enter the number of your desired region: +[7]: 11 +``` + +You will then be asked the remainder of the setup questions. + +## Using DigitalOcean with Algo (via Ansible) + +If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_access_token`. + +For example, + + ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2 + +Where "my_secret_token" is your API Token. diff --git a/docs/images/do-api.png b/docs/images/do-api.png new file mode 100644 index 0000000000000000000000000000000000000000..f3fccb788a4531c9338398801a3062aacb3302bf GIT binary patch literal 107121 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

Xb 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 L_-p?0^MU^ZhG^uJ literal 0 HcmV?d00001 diff --git a/docs/images/do-new-token.png b/docs/images/do-new-token.png new file mode 100644 index 0000000000000000000000000000000000000000..c05bc80b9e833df6c9150014c789b593b8e9cc55 GIT binary patch literal 114968 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≪1W7=3}cP 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 literal 0 HcmV?d00001 diff --git a/docs/images/do-view-token.png b/docs/images/do-view-token.png new file mode 100644 index 0000000000000000000000000000000000000000..402ed04aa046f5d65096284bb795e40b988aca13 GIT binary patch literal 152628 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

-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 UJ5RmeYKE+FQCp=@>FVwO0-LME1ONa4 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index b9b94bb6..e5c7050c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,7 @@ - Setup [Generic/Linux](client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](cloud-azure.md) + - Configure [DigitalOcean](cloud-do.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 16.04](deploy-to-ubuntu.md) server From f585a416df94e9823e0338528b19570b4a8f74c4 Mon Sep 17 00:00:00 2001 From: Matt Behrens Date: Mon, 23 Apr 2018 19:03:24 -0400 Subject: [PATCH 605/769] skip virtualenv check if already activated (#863) This allows the user to choose their virtualenv method, e.g. [Pipenv](https://docs.pipenv.org/). --- algo | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/algo b/algo index 403608a8..b451de37 100755 --- a/algo +++ b/algo @@ -2,13 +2,16 @@ set -e -ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" -if [ -f "$ACTIVATE_SCRIPT" ] +if [ -z ${VIRTUAL_ENV+x} ] then - source $ACTIVATE_SCRIPT -else - echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" - exit 1 + ACTIVATE_SCRIPT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env/bin/activate" + if [ -f "$ACTIVATE_SCRIPT" ] + then + source $ACTIVATE_SCRIPT + else + echo "$ACTIVATE_SCRIPT not found. Did you follow documentation to install dependencies?" + exit 1 + fi fi SKIP_TAGS="_null encrypted" From ed6e2d998da758edef3fe4456a47c7a4ff19bd3a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 24 Apr 2018 02:06:34 +0300 Subject: [PATCH 606/769] Add ipv6 address to subjectAltName if supported (#881) CHANGELOG Some changes Some changes --- CHANGELOG.md | 9 +++++++++ roles/vpn/defaults/main.yml | 3 +++ roles/vpn/tasks/openssl.yml | 14 +++++++++----- roles/vpn/templates/ipsec.conf.j2 | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..47b09d04 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +## 19 Apr 2018 +### Added +- IPv6 in subjectAltName of the certificates. This allows connecting to the Algo instance via the main IPv6 address + +### Fixed +- IPv6 DNS addresses were not passing to the client + +### Release notes +- In order to use the IPv6 address as the connection endpoint you need to [reinit](https://github.com/trailofbits/algo/blob/master/config.cfg#L14) the PKI and [reconfigure](https://github.com/trailofbits/algo#configure-the-vpn-clients) your devices with new certificates. diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 12f67887..2efc124d 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,4 +1,7 @@ --- +ipv6_support: false +domain: false +subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" openssl_bin: openssl strongswan_enabled_plugins: - aes diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 2457ea78..b4928cd9 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -1,11 +1,15 @@ --- - - block: + - name: Set subjectAltName as a fact + set_fact: + subjectAltName: "{{ subjectAltName_IP }}{% if ipv6_support and ansible_default_ipv6 %},IP:{{ ansible_default_ipv6['address'] }}{% endif %}{% if domain and subjectAltName_DNS %},DNS:{{ subjectAltName_DNS }}{% endif %}" + tags: always + - name: Ensure the pki directory does not exist file: dest: configs/{{ IP_subject_alt_name }}/pki state: absent - when: easyrsa_reinit_existent == True + when: easyrsa_reinit_existent|bool == True - name: Ensure the pki directories exist file: @@ -41,7 +45,7 @@ {{ openssl_bin }} ecparam -name prime256v1 -out ecparams/prime256v1.pem && {{ openssl_bin }} req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch @@ -68,7 +72,7 @@ shell: > {{ openssl_bin }} req -utf8 -new -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes -passin pass:"{{ easyrsa_CA_password }}" @@ -76,7 +80,7 @@ {{ 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=DNS:{{ IP_subject_alt_name }},IP:{{ IP_subject_alt_name }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -days 3650 -batch -passin pass:"{{ easyrsa_CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 6c5a2d45..1c4f6df2 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -31,7 +31,7 @@ conn %default {% if local_dns is defined and local_dns == "Y" %} rightdns={{ local_service_ip }} {% else %} - rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support is defined and ipv6_support == "yes" %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} {% endif %} conn ikev2-pubkey From 4bd59bebf4ebe30e30a776044500a16e93e97b9f Mon Sep 17 00:00:00 2001 From: Steven Crossan Date: Wed, 25 Apr 2018 02:42:23 +0000 Subject: [PATCH 607/769] Update DO doc link in README.md (#890) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54e72711..6e7ab679 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ After this process completes, the Algo VPN server will contains only the users l - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible * Cloud setup - Configure [Azure](docs/cloud-azure.md) - - Configure [DigitalOcean](cloud-do.md) + - Configure [DigitalOcean](docs/cloud-do.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server From c82bd8c5ffa2243650454c850f856a790a5cce3c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 25 Apr 2018 22:27:58 +0300 Subject: [PATCH 608/769] DNS-over-HTTPS (#875) --- algo | 2 +- config.cfg | 15 +- deploy.yml | 2 +- docs/client-linux.md | 2 +- docs/setup-roles.md | 3 + roles/common/tasks/ubuntu.yml | 10 +- roles/dns_adblocking/meta/main.yml | 3 + roles/dns_adblocking/tasks/freebsd.yml | 8 + roles/dns_adblocking/tasks/main.yml | 2 +- roles/dns_adblocking/tasks/ubuntu.yml | 4 +- .../dns_adblocking/templates/dnsmasq.conf.j2 | 6 +- roles/dns_encryption/defaults/main.yml | 7 + .../files/apparmor.profile.dnscrypt-proxy | 23 + .../dns_encryption/files/rc.dnscrypt-proxy.sh | 38 ++ roles/dns_encryption/handlers/main.yml | 9 + roles/dns_encryption/meta/main.yml | 4 + roles/dns_encryption/tasks/freebsd.yml | 51 ++ roles/dns_encryption/tasks/main.yml | 23 + roles/dns_encryption/tasks/ubuntu.yml | 48 ++ .../templates/dnscrypt-proxy.toml.j2 | 465 ++++++++++++++++++ roles/vpn/meta/main.yml | 4 +- roles/vpn/tasks/freebsd.yml | 2 +- roles/vpn/tasks/ubuntu.yml | 2 +- roles/vpn/templates/ipsec.conf.j2 | 2 +- tests/local-deploy.sh | 6 +- 25 files changed, 722 insertions(+), 19 deletions(-) create mode 100644 roles/dns_encryption/defaults/main.yml create mode 100644 roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy create mode 100644 roles/dns_encryption/files/rc.dnscrypt-proxy.sh create mode 100644 roles/dns_encryption/handlers/main.yml create mode 100644 roles/dns_encryption/meta/main.yml create mode 100644 roles/dns_encryption/tasks/freebsd.yml create mode 100644 roles/dns_encryption/tasks/main.yml create mode 100644 roles/dns_encryption/tasks/ubuntu.yml create mode 100644 roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 diff --git a/algo b/algo index b451de37..85b28a01 100755 --- a/algo +++ b/algo @@ -43,7 +43,7 @@ read -p " Do you want to install a DNS resolver on this VPN server, to block ads while surfing? [y/N]: " -r dns_enabled dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; fi +if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=true"; fi read -p " Do you want each user to have their own account for SSH tunneling? diff --git a/config.cfg b/config.cfg index d5cc0a55..6c38dc93 100644 --- a/config.cfg +++ b/config.cfg @@ -29,13 +29,20 @@ adblock_lists: - "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "https://hosts-file.net/ad_servers.txt" +# Enalbe DNS encryption. Use dns_encrypted_provider to specify the provider. If false dns_servers should be specified +dns_encryption: true + +# Possible values: google, cloudflare +dns_encryption_provider: cloudflare + +# DNS servers which will be used if dns_encryption disabled dns_servers: ipv4: - - 8.8.8.8 - - 8.8.4.4 + - 1.1.1.1 + - 1.0.0.1 ipv6: - - 2001:4860:4860::8888 - - 2001:4860:4860::8844 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 # IP address for the local dns resolver local_service_ip: 172.16.0.1 diff --git a/deploy.yml b/deploy.yml index fa5212ec..5ee93809 100644 --- a/deploy.yml +++ b/deploy.yml @@ -63,7 +63,7 @@ tags: always roles: - - { role: dns_adblocking, tags: ['dns', 'adblock' ] } + - { role: dns_adblocking, tags: [ 'dns', 'adblock' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - { role: vpn, tags: [ 'vpn' ] } diff --git a/docs/client-linux.md b/docs/client-linux.md index 954839cb..a24eda1d 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -64,7 +64,7 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * Certificate: `cacert.pem` found at `/path/to/algo/configs/1.2.3.4/cacert.pem` * Client: * Authentication: *Certificate/Private key* - * Certificate: `user-name.crt` found at `/path/to/algo/configs/1.2.3.4/pki/certs/user-name.crt` + * Certificate: `user-name.crt` found at `/path/to/algo/configs/1.2.3.4/pki/certs/user-name.crt` * Private key: `user-name.key` found at `/path/to/algo/configs/1.2.3.4/pki/private/user-name.key` * Options: * Check *Request an inner IP address*, connection will fail without this option diff --git a/docs/setup-roles.md b/docs/setup-roles.md index 697fc5f9..1523d181 100644 --- a/docs/setup-roles.md +++ b/docs/setup-roles.md @@ -20,6 +20,9 @@ * **DNS-based Adblocking** * Install the [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html) local resolver with a blacklist for advertising domains * Constrains dnsmasq with AppArmor and cgroups CPU and memory limitations +* **DNS encryption** + * Install [dnscrypt-proxy](https://github.com/jedisct1/dnscrypt-proxy) + * Constrains dingo with AppArmor and cgroups CPU and memory limitations * **SSH Tunneling** * Adds a restricted `algo` group with no shell access and limited SSH forwarding options * Creates one limited, local account per user and an SSH public key for each diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index ce337741..8b09374c 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -2,7 +2,15 @@ - name: Cloud only tasks block: - name: Install software updates - apt: update_cache=yes upgrade=dist + apt: + update_cache: true + install_recommends: true + upgrade: dist + + - name: Upgrade the ca certificates + apt: + name: ca-certificates + state: latest - name: Check if reboot is required shell: > diff --git a/roles/dns_adblocking/meta/main.yml b/roles/dns_adblocking/meta/main.yml index e985f927..5543bcab 100644 --- a/roles/dns_adblocking/meta/main.yml +++ b/roles/dns_adblocking/meta/main.yml @@ -2,3 +2,6 @@ dependencies: - { role: common, tags: common } + - role: dns_encryption + tags: dns_encryption + when: dns_encryption == true diff --git a/roles/dns_adblocking/tasks/freebsd.yml b/roles/dns_adblocking/tasks/freebsd.yml index a08e2342..1b73921a 100644 --- a/roles/dns_adblocking/tasks/freebsd.yml +++ b/roles/dns_adblocking/tasks/freebsd.yml @@ -2,3 +2,11 @@ - name: FreeBSD / HardenedBSD | Enable dnsmasq lineinfile: dest=/etc/rc.conf regexp=^dnsmasq_enable= line='dnsmasq_enable="YES"' + +- name: The dnsmasq additional directories created + file: + dest: "{{ item }}" + state: directory + mode: '0755' + with_items: + - "{{ config_prefix|default('/') }}etc/dnsmasq.d" diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index 43c06d5a..ded3f798 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -3,7 +3,7 @@ - name: The DNS tag is defined set_fact: - local_dns: Y + local_dns: true - name: Dnsmasq installed package: name=dnsmasq diff --git a/roles/dns_adblocking/tasks/ubuntu.yml b/roles/dns_adblocking/tasks/ubuntu.yml index 8e4cf3d0..ffc88876 100644 --- a/roles/dns_adblocking/tasks/ubuntu.yml +++ b/roles/dns_adblocking/tasks/ubuntu.yml @@ -7,13 +7,13 @@ owner: root group: root mode: 0600 - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true notify: - restart dnsmasq - name: Ubuntu | Enforce the dnsmasq AppArmor policy shell: aa-enforce usr.sbin.dnsmasq - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true tags: ['apparmor'] - name: Ubuntu | Ensure that the dnsmasq service directory exist diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index f92ee163..501f7568 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -88,9 +88,13 @@ no-resolv # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 +{% if dns_encryption|default(false)|bool == true %} +server={{ local_service_ip }}#5353 +{% else %} {% for host in dns_servers.ipv4 %} server={{ host }} {% endfor %} +{% endif %} # and this sets the source (ie local) address used to talk to # 10.1.2.3 to 192.168.1.1 port 55 (there must be a interface with that @@ -660,7 +664,7 @@ bind-interfaces # Include another lot of configuration options. #conf-file=/etc/dnsmasq.more.conf -conf-dir=/etc/dnsmasq.d +conf-dir={{ config_prefix|default('/') }}etc/dnsmasq.d/,*.conf # Include all the files in a directory except those ending in .bak #conf-dir=/etc/dnsmasq.d,.bak diff --git a/roles/dns_encryption/defaults/main.yml b/roles/dns_encryption/defaults/main.yml new file mode 100644 index 00000000..df031a90 --- /dev/null +++ b/roles/dns_encryption/defaults/main.yml @@ -0,0 +1,7 @@ +--- +listen_port: "{% if local_dns|d(false)|bool == true %}5353{% else %}53{% endif %}" +# the version used if the latest unavailable (in case of Github API rate limited) +dnscrypt_proxy_version: 2.0.10 +apparmor_enabled: true +dns_encryption: true +dns_encryption_provider: "*" diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy new file mode 100644 index 00000000..a2e51639 --- /dev/null +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -0,0 +1,23 @@ +#include + +/usr/sbin/dnscrypt-proxy { + #include + #include + #include + + capability chown, + capability dac_override, + capability net_bind_service, + capability setgid, + capability setuid, + capability sys_resource, + + /etc/dnscrypt-proxy.toml r, + /etc/ld.so.cache r, + /usr/sbin/dnscrypt-proxy mr, + /usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv r, + /usr/local/lib/{@{multiarch}/,}libldns.so* mr, + /usr/local/lib/{@{multiarch}/,}libsodium.so* mr, + /run/dnscrypt-proxy.pid rw, + /run/systemd/notify rw, +} diff --git a/roles/dns_encryption/files/rc.dnscrypt-proxy.sh b/roles/dns_encryption/files/rc.dnscrypt-proxy.sh new file mode 100644 index 00000000..da35d896 --- /dev/null +++ b/roles/dns_encryption/files/rc.dnscrypt-proxy.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# PROVIDE: dnscrypt-proxy +# REQUIRE: LOGIN +# BEFORE: securelevel +# KEYWORD: shutdown + +# Add the following lines to /etc/rc.conf to enable `dnscrypt-proxy': +# +# dnscrypt_proxy_enable="YES" +# dnscrypt_proxy_flags="" +# +# See rsync(1) for rsyncd_flags +# + +. /etc/rc.subr + +name="dnscrypt-proxy" +rcvar=dnscrypt_proxy_enable +load_rc_config "$name" +pidfile="/var/run/$name.pid" +start_cmd=dnscrypt_proxy_start +stop_postcmd=dnscrypt_proxy_stop + +: ${dnscrypt_proxy_enable="NO"} +: ${dnscrypt_proxy_flags="-config /usr/local/etc/dnscrypt-proxy/dnscrypt-proxy.toml"} + +dnscrypt_proxy_start() { + echo "Starting dnscrypt-proxy..." + touch ${pidfile} + /usr/sbin/daemon -cS -T dnscrypt-proxy -p ${pidfile} /usr/dnscrypt-proxy/freebsd-amd64/dnscrypt-proxy ${dnscrypt_proxy_flags} +} + +dnscrypt_proxy_stop() { + [ -f ${pidfile} ] && rm ${pidfile} +} + +run_rc_command "$1" diff --git a/roles/dns_encryption/handlers/main.yml b/roles/dns_encryption/handlers/main.yml new file mode 100644 index 00000000..c46912b9 --- /dev/null +++ b/roles/dns_encryption/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: daemon reload + systemd: + daemon_reload: true + +- name: restart dnscrypt-proxy + service: + name: dnscrypt-proxy + state: restarted diff --git a/roles/dns_encryption/meta/main.yml b/roles/dns_encryption/meta/main.yml new file mode 100644 index 00000000..9119c109 --- /dev/null +++ b/roles/dns_encryption/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: common + tags: common diff --git a/roles/dns_encryption/tasks/freebsd.yml b/roles/dns_encryption/tasks/freebsd.yml new file mode 100644 index 00000000..08e11905 --- /dev/null +++ b/roles/dns_encryption/tasks/freebsd.yml @@ -0,0 +1,51 @@ +--- +- name: FreeBSD | Ensure that the required directories exist + file: + path: "{{ item }}" + state: directory + with_items: + - "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/" + - /usr/dnscrypt-proxy/ + +- name: Required tools installed + package: + name: gtar + +- name: FreeBSD | Retrive the latest versions + uri: + url: https://api.github.com/repos/jedisct1/dnscrypt-proxy/releases/latest + register: dnscrypt_proxy_latest + ignore_errors: true + +- name: FreeBSD | Set default dnscrypt-proxy assets + set_fact: + dnscrypt_proxy_latest: + json: + assets: + - name: "dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" + browser_download_url: "https://github.com/jedisct1/dnscrypt-proxy/releases/download/{{ dnscrypt_proxy_version }}/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" + when: dnscrypt_proxy_latest.failed + +- name: FreeBSD | Download the latest archive + get_url: + url: "{{ item['browser_download_url'] }}" + dest: "/tmp/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" + mode: '0755' + force: true + with_items: "{{ dnscrypt_proxy_latest['json']['assets'] }}" + no_log: true + when: '"freebsd_amd64" in item.name' + notify: restart dnscrypt-proxy + +- name: FreeBSD | Extract the latest archive + unarchive: + remote_src: true + src: /tmp/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz + dest: /usr/dnscrypt-proxy + +- name: FreeBSD | Configure rc script + copy: + src: rc.dnscrypt-proxy.sh + dest: /usr/local/etc/rc.d/dnscrypt-proxy + mode: "0755" + notify: restart dnscrypt-proxy diff --git a/roles/dns_encryption/tasks/main.yml b/roles/dns_encryption/tasks/main.yml new file mode 100644 index 00000000..49c8d6e8 --- /dev/null +++ b/roles/dns_encryption/tasks/main.yml @@ -0,0 +1,23 @@ +--- +- name: Include tasks for Ubuntu + include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + +- name: Include tasks for FreeBSD + include_tasks: freebsd.yml + when: ansible_distribution == 'FreeBSD' + +- name: dnscrypt-proxy configured + template: + src: dnscrypt-proxy.toml.j2 + dest: "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/dnscrypt-proxy.toml" + notify: + - restart dnscrypt-proxy + +- name: dnscrypt-proxy enabled and started + service: + name: dnscrypt-proxy + state: started + enabled: true + +- meta: flush_handlers diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml new file mode 100644 index 00000000..7705a776 --- /dev/null +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -0,0 +1,48 @@ +--- +- name: Add the repository + apt_repository: + state: present + codename: artful + repo: ppa:shevchuk/dnscrypt-proxy + +- name: Install dnscrypt-proxy + apt: + name: dnscrypt-proxy + state: latest + update_cache: true + +- block: + - name: Ubuntu | Unbound profile for apparmor configured + copy: + src: apparmor.profile.dnscrypt-proxy + dest: /etc/apparmor.d/usr.sbin.dnscrypt-proxy + owner: root + group: root + mode: 0600 + notify: restart dnscrypt-proxy + + - name: Ubuntu | Enforce the dnscrypt-proxy AppArmor policy + command: aa-enforce usr.sbin.dnscrypt-proxy + changed_when: false + tags: apparmor + when: apparmor_enabled|default(false)|bool == true + +- name: Ubuntu | Ensure that the dnscrypt-proxy service directory exist + file: + path: /etc/systemd/system/dnscrypt-proxy.service.d/ + state: directory + mode: 0755 + owner: root + group: root + +- name: Ubuntu | Setup the cgroup limitations for dnscrypt-proxy + copy: + dest: /etc/systemd/system/dnscrypt-proxy.service.d/100-CustomLimitations.conf + content: | + [Service] + MemoryLimit=16777216 + CPUAccounting=true + CPUQuota=5% + notify: + - daemon-reload + - restart dnscrypt-proxy diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 new file mode 100644 index 00000000..5afeb2ef --- /dev/null +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -0,0 +1,465 @@ + +############################################## +# # +# dnscrypt-proxy configuration # +# # +############################################## + +## This is an example configuration file. +## You should adjust it to your needs, and save it as "dnscrypt-proxy.toml" +## +## Online documentation is available here: https://dnscrypt.info/doc + + + +################################## +# Global settings # +################################## + +## List of servers to use +## +## Servers from the "public-resolvers" source (see down below) can +## be viewed here: https://dnscrypt.info/public-servers +## +## If this line is commented, all registered servers matching the require_* filters +## will be used. +## +## The proxy will automatically pick the fastest, working servers from the list. +## Remove the leading # first to enable this; lines starting with # are ignored. + +server_names = ['{{ dns_encryption_provider }}'{% if ipv6_support|d(false)|bool == true and dns_encryption_provider == "cloudflare" %}, '{{ dns_encryption_provider }}-ipv6' {% endif %} ] + + +## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. +## Note: When using systemd socket activation, choose an empty set (i.e. [] ). + +listen_addresses = ['{{ local_service_ip }}:{{ listen_port }}'] + + +## Maximum number of simultaneous client connections to accept + +max_clients = 250 + + +## Require servers (from static + remote sources) to satisfy specific properties + +# Use servers reachable over IPv4 +ipv4_servers = true + +# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity +ipv6_servers = {{ ipv6_support|default(false) | bool | lower }} + +# Use servers implementing the DNSCrypt protocol +dnscrypt_servers = true + +# Use servers implementing the DNS-over-HTTPS protocol +doh_servers = true + + +## Require servers defined by remote sources to satisfy specific properties + +# Server must support DNS security extensions (DNSSEC) +require_dnssec = true + +# Server must not log user queries (declarative) +require_nolog = true + +# Server must not enforce its own blacklist (for parental control, ads blocking...) +require_nofilter = true + + + +## Always use TCP to connect to upstream servers + +force_tcp = false + + +## How long a DNS query will wait for a response, in milliseconds + +timeout = 2500 + + +## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds + +keepalive = 30 + + +## Load-balancing strategy: 'p2' (default), 'ph', 'fastest' or 'random' + +lb_strategy = 'p2' + + +## Log level (0-6, default: 2 - 0 is very verbose, 6 only contains fatal errors) + +log_level = 2 + + +## log file for the application + +# log_file = 'dnscrypt-proxy.log' + + +## Use the system logger (syslog on Unix, Event Log on Windows) + +use_syslog = true + + +## Delay, in minutes, after which certificates are reloaded + +cert_refresh_delay = 240 + + +## DNSCrypt: Create a new, unique key for every single DNS query +## This may improve privacy but can also have a significant impact on CPU usage +## Only enable if you don't have a lot of network load + +dnscrypt_ephemeral_keys = true + + +## DoH: Disable TLS session tickets - increases privacy but also latency + +tls_disable_session_tickets = true + + +## DoH: Use a specific cipher suite instead of the server preference +## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 +## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 +## +## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...), +## the following suite improves performance. +## This may also help on Intel CPUs running 32-bit operating systems. +## +## Keep tls_cipher_suite empty if you have issues fetching sources or +## connecting to some DoH servers. Google and Cloudflare are fine with it. + +tls_cipher_suite = [49195] + + +## Fallback resolver +## This is a normal, non-encrypted DNS resolver, that will be only used +## for one-shot queries when retrieving the initial resolvers list, and +## only if the system DNS configuration doesn't work. +## No user application queries will ever be leaked through this resolver, +## and it will not be used after IP addresses of resolvers URLs have been found. +## It will never be used if lists have already been cached, and if stamps +## don't include host names without IP addresses. +## It will not be used if the configured system DNS works. +## A resolver supporting DNSSEC is recommended. This may become mandatory. +## +## People in China may need to use 114.114.114.114:53 here. +## Other popular options include 8.8.8.8 and 1.1.1.1. + +fallback_resolver = '1.1.1.1:53' + + +## Never try to use the system DNS settings; unconditionally use the +## fallback resolver. + +ignore_system_dns = true + + +## Automatic log files rotation + +# Maximum log files size in MB +log_files_max_size = 10 + +# How long to keep backup files, in days +log_files_max_age = 7 + +# Maximum log files backups to keep (or 0 to keep all backups) +log_files_max_backups = 1 + + + +######################### +# Filters # +######################### + +## Immediately respond to IPv6-related queries with an empty response +## This makes things faster when there is no IPv6 connectivity, but can +## also cause reliability issues with some stub resolvers. In +## particular, enabling this on macOS is not recommended. + +block_ipv6 = false + + + +################################################################################## +# Route queries for specific domains to a dedicated set of servers # +################################################################################## + +## Example map entries (one entry per line): +## example.com 9.9.9.9 +## example.net 9.9.9.9,8.8.8.8,1.1.1.1 + +# forwarding_rules = 'forwarding-rules.txt' + + + +############################### +# Cloaking rules # +############################### + +## Cloaking returns a predefined address for a specific name. +## In addition to acting as a HOSTS file, it can also return the IP address +## of a different name. It will also do CNAME flattening. +## +## Example map entries (one entry per line) +## example.com 10.1.1.1 +## www.google.com forcesafesearch.google.com + +# cloaking_rules = 'cloaking-rules.txt' + + + +########################### +# DNS cache # +########################### + +## Enable a DNS cache to reduce latency and outgoing traffic + +cache = true + + +## Cache size + +cache_size = 512 + + +## Minimum TTL for cached entries + +cache_min_ttl = 600 + + +## Maximum TTL for cached entries + +cache_max_ttl = 86400 + + +## TTL for negatively cached entries + +cache_neg_ttl = 60 + + + +############################### +# Query logging # +############################### + +## Log client queries to a file + +[query_log] + + ## Path to the query log file (absolute, or relative to the same directory as the executable file) + + # file = 'query.log' + + + ## Query log format (currently supported: tsv and ltsv) + + format = 'tsv' + + + ## Do not log these query types, to reduce verbosity. Keep empty to log everything. + + # ignored_qtypes = ['DNSKEY', 'NS'] + + + +############################################ +# Suspicious queries logging # +############################################ + +## Log queries for nonexistent zones +## These queries can reveal the presence of malware, broken/obsolete applications, +## and devices signaling their presence to 3rd parties. + +[nx_log] + + ## Path to the query log file (absolute, or relative to the same directory as the executable file) + + # file = 'nx.log' + + + ## Query log format (currently supported: tsv and ltsv) + + format = 'tsv' + + + +###################################################### +# Pattern-based blocking (blacklists) # +###################################################### + +## Blacklists are made of one pattern per line. Example of valid patterns: +## +## example.com +## =example.com +## *sex* +## ads.* +## ads*.example.* +## ads*.example[0-9]*.com +## +## Example blacklist files can be found at https://download.dnscrypt.info/blacklists/ +## A script to build blacklists from public feeds can be found in the +## `utils/generate-domains-blacklists` directory of the dnscrypt-proxy source code. + +[blacklist] + + ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) + + # blacklist_file = 'blacklist.txt' + + + ## Optional path to a file logging blocked queries + + # log_file = 'blocked.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +########################################################### +# Pattern-based IP blocking (IP blacklists) # +########################################################### + +## IP blacklists are made of one pattern per line. Example of valid patterns: +## +## 127.* +## fe80:abcd:* +## 192.168.1.4 + +[ip_blacklist] + + ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) + + # blacklist_file = 'ip-blacklist.txt' + + + ## Optional path to a file logging blocked queries + + # log_file = 'ip-blocked.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +###################################################### +# Pattern-based whitelisting (blacklists bypass) # +###################################################### + +## Whitelists support the same patterns as blacklists +## If a name matches a whitelist entry, the corresponding session +## will bypass names and IP filters. +## +## Time-based rules are also supported to make some websites only accessible at specific times of the day. + +[whitelist] + + ## Path to the file of whitelisting rules (absolute, or relative to the same directory as the executable file) + + # whitelist_file = 'whitelist.txt' + + + ## Optional path to a file logging whitelisted queries + + # log_file = 'whitelisted.log' + + + ## Optional log format: tsv or ltsv (default: tsv) + + # log_format = 'tsv' + + + +########################################## +# Time access restrictions # +########################################## + +## One or more weekly schedules can be defined here. +## Patterns in the name-based blocklist can optionally be followed with @schedule_name +## to apply the pattern 'schedule_name' only when it matches a time range of that schedule. +## +## For example, the following rule in a blacklist file: +## *.youtube.* @time-to-sleep +## would block access to YouTube only during the days, and period of the days +## define by the 'time-to-sleep' schedule. +## +## {after='21:00', before= '7:00'} matches 0:00-7:00 and 21:00-0:00 +## {after= '9:00', before='18:00'} matches 9:00-18:00 + +[schedules] + + # [schedules.'time-to-sleep'] + # mon = [{after='21:00', before='7:00'}] + # tue = [{after='21:00', before='7:00'}] + # wed = [{after='21:00', before='7:00'}] + # thu = [{after='21:00', before='7:00'}] + # fri = [{after='23:00', before='7:00'}] + # sat = [{after='23:00', before='7:00'}] + # sun = [{after='21:00', before='7:00'}] + + # [schedules.'work'] + # mon = [{after='9:00', before='18:00'}] + # tue = [{after='9:00', before='18:00'}] + # wed = [{after='9:00', before='18:00'}] + # thu = [{after='9:00', before='18:00'}] + # fri = [{after='9:00', before='17:00'}] + + + +######################### +# Servers # +######################### + +## Remote lists of available servers +## Multiple sources can be used simultaneously, but every source +## requires a dedicated cache file. +## +## Refer to the documentation for URLs of public sources. +## +## A prefix can be prepended to server names in order to +## avoid collisions if different sources share the same for +## different servers. In that case, names listed in `server_names` +## must include the prefixes. +## +## If the `urls` property is missing, cache files and valid signatures +## must be already present; This doesn't prevent these cache files from +## expiring after `refresh_delay` hours. + +[sources] + + ## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers + + [sources.'public-resolvers'] + urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md'] + cache_file = 'public-resolvers.md' + minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + refresh_delay = 72 + prefix = '' + + ## Another example source, with resolvers censoring some websites not appropriate for children + ## This is a subset of the `public-resolvers` list, so enabling both is useless + + # [sources.'parental-control'] + # urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v2/parental-control.md'] + # cache_file = 'parental-control.md' + # minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' + + + +## Optional, local, static list of additional servers +## Mostly useful for testing your own servers. + +[static] + + # [static.'google'] + # stamp = 'sdns://AgUAAAAAAAAAAAAOZG5zLmdvb2dsZS5jb20NL2V4cGVyaW1lbnRhbA' diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index f3d19204..5543bcab 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -2,4 +2,6 @@ dependencies: - { role: common, tags: common } - + - role: dns_encryption + tags: dns_encryption + when: dns_encryption == true diff --git a/roles/vpn/tasks/freebsd.yml b/roles/vpn/tasks/freebsd.yml index 1dbecd5f..43cfbf63 100644 --- a/roles/vpn/tasks/freebsd.yml +++ b/roles/vpn/tasks/freebsd.yml @@ -24,7 +24,7 @@ line: "{{ item }}" insertbefore: BOF with_items: - - "options IPSEC" + - "options IPSEC" - "options IPSEC_NAT_T" - "device crypto" when: rebuild_needed is defined and rebuild_needed == true diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/vpn/tasks/ubuntu.yml index d3a858ca..6f585441 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/vpn/tasks/ubuntu.yml @@ -12,7 +12,7 @@ - name: Ubuntu | Enforcing ipsec with apparmor shell: aa-enforce "{{ item }}" - when: apparmor_enabled is defined and apparmor_enabled == true + when: apparmor_enabled|default(false)|bool == true with_items: - /usr/lib/ipsec/charon - /usr/lib/ipsec/lookip diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 1c4f6df2..e98bb3c1 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -28,7 +28,7 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if local_dns is defined and local_dns == "Y" %} +{% if local_dns|d(false)|bool == true or dns_encryption|d(false)|bool == true %} rightdns={{ local_service_ip }} {% else %} rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index 7779aef8..5cb7c3f2 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,11 +2,11 @@ set -ex -DEPLOY_ARGS="server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=Y" +DEPLOY_ARGS="server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false" if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,security,tests -e \"${DEPLOY_ARGS}\"" + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" else - ansible-playbook deploy.yml -t local,vpn,dns,ssh_tunneling,tests -e "${DEPLOY_ARGS}" -vvvv + ansible-playbook deploy.yml -t cloud,local,vpn,dns,dns_over_https,ssh_tunneling,tests -e "${DEPLOY_ARGS}" --skip-tags apparmor fi From c276f971b7e12537a26e2b4e8e3dcccbb6e554ad Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Wed, 25 Apr 2018 15:32:50 -0700 Subject: [PATCH 609/769] monkey patch problematic dnscrypt-proxy cgroup limits (#894) --- roles/dns_encryption/tasks/ubuntu.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 7705a776..a543f842 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -35,14 +35,14 @@ owner: root group: root -- name: Ubuntu | Setup the cgroup limitations for dnscrypt-proxy - copy: - dest: /etc/systemd/system/dnscrypt-proxy.service.d/100-CustomLimitations.conf - content: | - [Service] - MemoryLimit=16777216 - CPUAccounting=true - CPUQuota=5% - notify: - - daemon-reload - - restart dnscrypt-proxy +#- name: Ubuntu | Setup the cgroup limitations for dnscrypt-proxy +# copy: +# dest: /etc/systemd/system/dnscrypt-proxy.service.d/100-CustomLimitations.conf +# content: | +# [Service] +# MemoryLimit=16777216 +# CPUAccounting=true +# CPUQuota=5% +# notify: +# - daemon-reload +# - restart dnscrypt-proxy From cfc985c776bdde438dc1e94d0f660155caf3d854 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 27 Apr 2018 10:06:51 +0300 Subject: [PATCH 610/769] DNS-crypt changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b09d04..78644798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 25 Apr 2018 +### Added +- DNScrypt-proxy added +- Switched to CloudFlare DNS-over-HTTPS by default + ## 19 Apr 2018 ### Added - IPv6 in subjectAltName of the certificates. This allows connecting to the Algo instance via the main IPv6 address From 3d9fa7f8c85ba83a4a46ccbdd385d9fb6a2b1441 Mon Sep 17 00:00:00 2001 From: adamluk Date: Fri, 27 Apr 2018 15:29:29 +0100 Subject: [PATCH 611/769] Update dnscrypt-proxy.toml.j2 (#899) Updated dnscrypt-proxy.tml with new options: cache_neg_min_ttl and cache_neg_max_ttl --- roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 5afeb2ef..03fa1aa3 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -238,9 +238,14 @@ cache_min_ttl = 600 cache_max_ttl = 86400 -## TTL for negatively cached entries +## Minimum TTL for negatively cached entries -cache_neg_ttl = 60 +cache_neg_min_ttl = 60 + + +## Maximum TTL for negatively cached entries + +cache_neg_max_ttl = 600 From e01e82b1c30bfea2d28c7c85f1e05b67c75f58f8 Mon Sep 17 00:00:00 2001 From: Brian Hulette Date: Sun, 29 Apr 2018 13:32:10 -0400 Subject: [PATCH 612/769] Don't download minisig dnscrypt release (#905) --- roles/dns_encryption/tasks/freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_encryption/tasks/freebsd.yml b/roles/dns_encryption/tasks/freebsd.yml index 08e11905..30e0186c 100644 --- a/roles/dns_encryption/tasks/freebsd.yml +++ b/roles/dns_encryption/tasks/freebsd.yml @@ -34,7 +34,7 @@ force: true with_items: "{{ dnscrypt_proxy_latest['json']['assets'] }}" no_log: true - when: '"freebsd_amd64" in item.name' + when: '"freebsd_amd64" in item.name and not item.name.endswith("minisig")' notify: restart dnscrypt-proxy - name: FreeBSD | Extract the latest archive From 3945a0e286040ff99e9c695aaa42b0b27d15ffc9 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 30 Apr 2018 09:29:43 +0300 Subject: [PATCH 613/769] Typo --- algo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo b/algo index 85b28a01..a426e9b8 100755 --- a/algo +++ b/algo @@ -382,7 +382,7 @@ algo_region=${algo_region:-1} openstack () { read -p " -Enter the local path to your credentials OpenStack RC file (Can be donloaded from the OpenStack dashboard->Compute->API Access) +Enter the local path to your credentials OpenStack RC file (Can be downloaded from the OpenStack dashboard->Compute->API Access) [...]: " -r os_rc read -p " From 53ef2fcaa75c17590efbd506a55d94bae0d315a7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 3 May 2018 16:04:39 +0300 Subject: [PATCH 614/769] Increase SSH retries (#909) --- ansible.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible.cfg b/ansible.cfg index 8c63b5ea..c4d18d19 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -11,3 +11,4 @@ record_host_keys = False [ssh_connection] ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o ConnectTimeout=6 -o ConnectionAttempts=30 -o IdentitiesOnly=yes scp_if_ssh = True +retries = 30 From 616b849b98e887c8561f57bdd85446194139c0fa Mon Sep 17 00:00:00 2001 From: pguizeline Date: Tue, 8 May 2018 17:07:06 -0300 Subject: [PATCH 615/769] Add new EC2 regions (#928) Adds new EC2 regions according to: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions --- algo | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/algo b/algo index a426e9b8..c6a491fa 100755 --- a/algo +++ b/algo @@ -252,16 +252,18 @@ Name the vpn server: 2. us-east-2 US East (Ohio) 3. us-west-1 US West (N. California) 4. us-west-2 US West (Oregon) - 5. ap-south-1 Asia Pacific (Mumbai) - 6. ap-northeast-2 Asia Pacific (Seoul) - 7. ap-southeast-1 Asia Pacific (Singapore) - 8. ap-southeast-2 Asia Pacific (Sydney) - 9. ap-northeast-1 Asia Pacific (Tokyo) - 10. eu-central-1 EU (Frankfurt) - 11. eu-west-1 EU (Ireland) - 12. eu-west-2 EU (London) - 13. ca-central-1 Canada (Central) - 14. sa-east-1 São Paulo + 5. ca-central-1 Canada (Central) + 6. eu-central-1 EU (Frankfurt) + 7. eu-west-1 EU (Ireland) + 8. eu-west-2 EU (London) + 9. eu-west-3 EU (Paris) + 10. ap-northeast-1 Asia Pacific (Tokyo) + 11. ap-northeast-2 Asia Pacific (Seoul) + 12. ap-northeast-3 Asia Pacific (Osaka-Local) + 13. ap-southeast-1 Asia Pacific (Singapore) + 14. ap-southeast-2 Asia Pacific (Sydney) + 15. ap-south-1 Asia Pacific (Mumbai) + 16. sa-east-1 South America (São Paulo) Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} @@ -271,16 +273,18 @@ Enter the number of your desired region: 2) region="us-east-2" ;; 3) region="us-west-1" ;; 4) region="us-west-2" ;; - 5) region="ap-south-1" ;; - 6) region="ap-northeast-2" ;; - 7) region="ap-southeast-1" ;; - 8) region="ap-southeast-2" ;; - 9) region="ap-northeast-1" ;; - 10) region="eu-central-1" ;; - 11) region="eu-west-1" ;; - 12) region="eu-west-2";; - 13) region="ca-central-1" ;; - 14) region="sa-east-1" ;; + 5) region="ca-central-1" ;; + 6) region="eu-central-1" ;; + 7) region="eu-west-1" ;; + 8) region="eu-west-2" ;; + 9) region="eu-west-3" ;; + 10) region="ap-northeast-1" ;; + 11) region="ap-northeast-2" ;; + 12) region="ap-northeast-3";; + 13) region="ap-southeast-1" ;; + 14) region="ap-southeast-2" ;; + 15) region="ap-south-1" ;; + 16) region="sa-east-1" ;; esac ROLES="ec2 vpn cloud" From 499c1951296d38226c8460459dc3b38a1a833dad Mon Sep 17 00:00:00 2001 From: pguizeline Date: Tue, 8 May 2018 17:07:27 -0300 Subject: [PATCH 616/769] Add new Azure locations (#929) Reorganized and added new locations. https://azure.microsoft.com/en-us/global-infrastructure/locations/ https://azure.microsoft.com/en-us/global-infrastructure/services/ --- algo | 118 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/algo b/algo index c6a491fa..aaa38c06 100755 --- a/algo +++ b/algo @@ -108,68 +108,76 @@ Name the vpn server: read -p " What region should the server be located in? (https://azure.microsoft.com/en-us/regions/) - 1. South Central US - 2. Central US - 3. North Europe - 4. West Europe - 5. Southeast Asia - 6. Japan West - 7. Japan East - 8. Australia Southeast - 9. Australia East - 10. Canada Central - 11. West US 2 - 12. West Central US - 13. UK South - 14. UK West - 15. West US - 16. Brazil South - 17. Canada East - 18. Central India - 19. East Asia - 20. Germany Central - 21. Germany Northeast - 22. Korea Central - 23. Korea South - 24. North Central US - 25. South India - 26. West India - 27. East US - 28. East US 2 + 1. East US (Virginia) + 2. East US 2 (Virginia) + 3. Central US (Iowa) + 4. North Central US (Illinois) + 5. South Central US (Texas) + 6. West Central US (Wyoming) + 7. West US (California) + 8. West US 2 (Washington) + 9. Canada East (Quebec City) + 10. Canada Central (Toronto) + 11. Brazil South (Sao Paulo State) + 12. North Europe (Ireland) + 13. West Europe (Netherlands) + 14. France Central (Paris) + 15. France South (Marseille) + 16. UK West (Cardiff) + 17. UK South (London) + 18. Germany Central (Frankfurt) + 19. Germany Northeast (Magdeburg) + 20. Southeast Asia (Singapore) + 21. East Asia (Hong Kong) + 22. Australia East (New South Wales) + 23. Australia Southeast (Victoria) + 24. Australia Central (Canberra) + 25. Australia Central 2 (Canberra) + 26. Central India (Pune) + 27. West India (Mumbai) + 28. South India (Chennai) + 29. Japan East (Tokyo, Saitama) + 30. Japan West (Osaka) + 31. Korea Central (Seoul) + 32. Korea South (Busan) Enter the number of your desired region: [1]: " -r azure_region azure_region=${azure_region:-1} case "$azure_region" in - 1) region="southcentralus" ;; - 2) region="centralus" ;; - 3) region="northeurope" ;; - 4) region="westeurope" ;; - 5) region="southeastasia" ;; - 6) region="japanwest" ;; - 7) region="japaneast" ;; - 8) region="australiasoutheast" ;; - 9) region="australiaeast" ;; + 1) region="eastus" ;; + 2) region="eastus2" ;; + 3) region="centralus" ;; + 4) region="northcentralus" ;; + 5) region="southcentralus" ;; + 6) region="westcentralus" ;; + 7) region="westus" ;; + 8) region="westus2" ;; + 9) region="canadaeast" ;; 10) region="canadacentral" ;; - 11) region="westus2" ;; - 12) region="westcentralus" ;; - 13) region="uksouth" ;; - 14) region="ukwest" ;; - 15) region="westus" ;; - 16) region="brazilsouth" ;; - 17) region="canadaeast" ;; - 18) region="centralindia" ;; - 19) region="eastasia" ;; - 20) region="germanycentral" ;; - 21) region="germanynortheast" ;; - 22) region="koreacentral" ;; - 23) region="koreasouth" ;; - 24) region="northcentralus" ;; - 25) region="southindia" ;; - 26) region="westindia" ;; - 27) region="eastus" ;; - 28) region="eastus2" ;; + 11) region="brazilsouth" ;; + 12) region="northeurope" ;; + 13) region="westeurope" ;; + 14) region="francecentral" ;; + 15) region="francesouth" ;; + 16) region="ukwest" ;; + 17) region="uksouth" ;; + 18) region="germanycentral" ;; + 19) region="germanynortheast" ;; + 20) region="southeastasia" ;; + 21) region="eastasia" ;; + 22) region="australiaeast" ;; + 23) region="australiasoutheast" ;; + 24) region="australiacentral" ;; + 25) region="australiacentral2" ;; + 26) region="centralindia" ;; + 27) region="westindia" ;; + 28) region="southindia" ;; + 29) region="japaneast" ;; + 30) region="japanwest" ;; + 31) region="koreacentral" ;; + 32) region="koreasouth" ;; esac ROLES="azure vpn cloud" From 35e526a5a3cabf406cb7762dad8363f4fb0e6ab7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 8 May 2018 23:55:17 +0300 Subject: [PATCH 617/769] IPv6 fixes (#930) --- playbooks/facts/main.yml | 5 ++--- roles/cloud-azure/tasks/main.yml | 1 - roles/cloud-digitalocean/tasks/main.yml | 1 - roles/cloud-ec2/tasks/main.yml | 1 - roles/cloud-gce/tasks/main.yml | 1 - roles/cloud-lightsail/tasks/main.yml | 1 - roles/cloud-openstack/tasks/main.yml | 1 - roles/cloud-scaleway/tasks/main.yml | 1 - roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 | 4 ++-- roles/vpn/tasks/iptables.yml | 2 +- roles/vpn/tasks/openssl.yml | 2 +- 11 files changed, 6 insertions(+), 14 deletions(-) diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml index 02d991ff..a03e7810 100644 --- a/playbooks/facts/main.yml +++ b/playbooks/facts/main.yml @@ -10,10 +10,9 @@ key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" tags: [ 'cloud' ] -- name: Enable IPv6 +- name: Check if IPv6 configured set_fact: - ipv6_support: true - when: ansible_default_ipv6.gateway is defined + ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" - name: Set facts if the deployment in a cloud set_fact: diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 4cf621fa..bee7e982 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -118,7 +118,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: azure - ipv6_support: no - set_fact: cloud_instance_ip: "{{ ip_address }}" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index 3fec1e4c..f4932998 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -64,7 +64,6 @@ do_access_token: "{{ do_token }}" do_droplet_id: "{{ do.droplet.id }}" cloud_provider: digitalocean - ipv6_support: true - set_fact: cloud_instance_ip: "{{ do.droplet.ip_address }}" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 7d5894c7..0e820b84 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -32,7 +32,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: ec2 - ipv6_support: yes - set_fact: cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index f198b7ab..dafa7553 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -46,7 +46,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: gce - ipv6_support: no - set_fact: cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index ce28ceb4..437e8448 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -43,7 +43,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: lightsail - ipv6_support: no rescue: - debug: var=fail_hint diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index aef49a5b..63dbb726 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -78,7 +78,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: openstack - ipv6_support: omit rescue: - debug: var=fail_hint diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index ca4e4e6d..805b4de4 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -119,7 +119,6 @@ ansible_python_interpreter: "/usr/bin/python2.7" ansible_ssh_private_key_file: "{{ SSH_keys.private }}" cloud_provider: scaleway - ipv6_support: yes rescue: - debug: var=fail_hint diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 03fa1aa3..72eb898d 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -27,7 +27,7 @@ ## The proxy will automatically pick the fastest, working servers from the list. ## Remove the leading # first to enable this; lines starting with # are ignored. -server_names = ['{{ dns_encryption_provider }}'{% if ipv6_support|d(false)|bool == true and dns_encryption_provider == "cloudflare" %}, '{{ dns_encryption_provider }}-ipv6' {% endif %} ] +server_names = ['{{ dns_encryption_provider }}'{% if ipv6_support and dns_encryption_provider == "cloudflare" %}, '{{ dns_encryption_provider }}-ipv6' {% endif %} ] ## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. @@ -47,7 +47,7 @@ max_clients = 250 ipv4_servers = true # Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity -ipv6_servers = {{ ipv6_support|default(false) | bool | lower }} +ipv6_servers = {{ ipv6_support | bool | lower }} # Use servers implementing the DNSCrypt protocol dnscrypt_servers = true diff --git a/roles/vpn/tasks/iptables.yml b/roles/vpn/tasks/iptables.yml index 251335d6..e5b10619 100644 --- a/roles/vpn/tasks/iptables.yml +++ b/roles/vpn/tasks/iptables.yml @@ -19,7 +19,7 @@ owner: root group: root mode: 0640 - when: ipv6_support is defined and ipv6_support == true + when: ipv6_support with_items: - { src: rules.v6.j2, dest: /etc/iptables/rules.v6 } notify: diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index b4928cd9..8dbd4ef8 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -2,7 +2,7 @@ - block: - name: Set subjectAltName as a fact set_fact: - subjectAltName: "{{ subjectAltName_IP }}{% if ipv6_support and ansible_default_ipv6 %},IP:{{ ansible_default_ipv6['address'] }}{% endif %}{% if domain and subjectAltName_DNS %},DNS:{{ subjectAltName_DNS }}{% endif %}" + subjectAltName: "{{ subjectAltName_IP }}{% if ipv6_support %},IP:{{ ansible_default_ipv6['address'] }}{% endif %}{% if domain and subjectAltName_DNS %},DNS:{{ subjectAltName_DNS }}{% endif %}" tags: always - name: Ensure the pki directory does not exist From daf609ea0324d762569a84bbeb3583277ed82f12 Mon Sep 17 00:00:00 2001 From: pguizeline Date: Tue, 8 May 2018 17:57:21 -0300 Subject: [PATCH 618/769] Update README.md (#931) - Adds missing providers to the documentation with links. - Mentions that your own server install needs to be an Ubuntu 16.04 LTS distro - Emphasize that the p12 certificate password will only be available once --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e7ab679..53619d10 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Microsoft Azure, Google Compute Engine, or your own server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 16.04 LTS server ## Anti-features @@ -29,7 +29,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Google Compute Engine](https://cloud.google.com/compute/), and [Microsoft Azure](https://azure.microsoft.com/). +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon Lightsail](https://aws.amazon.com/lightsail/), [Amazon EC2](https://aws.amazon.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [OpenStack](https://www.openstack.org/). 2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. @@ -67,7 +67,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua 6. **Start the deployment.** Return to your terminal. In the Algo directory, run `./algo` and follow the instructions. There are several optional features available. None are required for a fully functional VPN server. These optional features are described in greater detail in [deploy-from-ansible.md](docs/deploy-from-ansible.md). -That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later. +That's it! You will get the message below when the server deployment process completes. You now have an Algo server on the internet. Take note of the p12 (user certificate) password in case you need it later, **it will only be displayed this time**. You can now setup clients to connect it, e.g. your iPhone or laptop. Proceed to [Configure the VPN Clients](#configure-the-vpn-clients) below. From e95ae829e305378b76cc69a79a91b4d24a567c43 Mon Sep 17 00:00:00 2001 From: pguizeline Date: Wed, 9 May 2018 15:25:14 -0300 Subject: [PATCH 619/769] Fix line spacing to improve readability (#932) Keeping the organized spacing --- algo | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/algo b/algo index aaa38c06..7b4d4377 100755 --- a/algo +++ b/algo @@ -211,6 +211,7 @@ Name the vpn server: 10. Singapore 11. Toronto 12. Bangalore + Enter the number of your desired region: [7]: " -r region region=${region:-7} @@ -272,6 +273,7 @@ Name the vpn server: 14. ap-southeast-2 Asia Pacific (Sydney) 15. ap-south-1 Asia Pacific (Mumbai) 16. sa-east-1 South America (São Paulo) + Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} @@ -333,6 +335,7 @@ Name the vpn server: 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) 12. eu-west-2 EU (London) + Enter the number of your desired region: [1]: " -r algo_region algo_region=${algo_region:-1} @@ -458,6 +461,7 @@ Name the vpn server: 34. South America (São Paulo A) 35. South America (São Paulo B) 36. South America (São Paulo C) + Please choose the number of your zone. Press enter for default (#14) zone. [14]: " -r region region=${region:-14} From e905220f61dfb32ef69281a2f209dfc94c20b3f2 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Wed, 9 May 2018 16:14:31 -0400 Subject: [PATCH 620/769] Update config.cfg (#936) Fix typos - this puzzled me when I was attempting to install algo with dnscrypt last week. --- config.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.cfg b/config.cfg index 6c38dc93..02a9ec54 100644 --- a/config.cfg +++ b/config.cfg @@ -29,7 +29,7 @@ adblock_lists: - "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "https://hosts-file.net/ad_servers.txt" -# Enalbe DNS encryption. Use dns_encrypted_provider to specify the provider. If false dns_servers should be specified +# Enable DNS encryption. Use dns_encryption_provider to specify the provider. If false dns_servers should be specified dns_encryption: true # Possible values: google, cloudflare From 6f3ec658fe73ce2d85e60d628c9aaafb882d186b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 10 May 2018 09:03:05 +0300 Subject: [PATCH 621/769] Move to LXD (#935) --- .travis.yml | 35 +++++++++++++++++++---------------- tests/local-deploy.sh | 2 +- tests/lxd-bridge | 16 ++++++++++++++++ tests/update-users.sh | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 tests/lxd-bridge diff --git a/.travis.yml b/.travis.yml index 6971d1ff..e3ccf43a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,15 +13,20 @@ matrix: addons: apt: sources: - - sourceline: 'ppa:ubuntu-lxc/stable' + - sourceline: 'ppa:ubuntu-lxc/stable' packages: - - python-pip - - lxc - - lxc-templates - - expect-dev - - debootstrap - - shellcheck - - tree + - python-pip + - lxd + - expect-dev + - debootstrap + - shellcheck + - tree + - bridge-utils + - dnsutils + - build-essential + - libssl-dev + - libffi-dev + - python-dev cache: directories: @@ -43,16 +48,14 @@ before_install: install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." - - export LXC_ROOTFS=/var/lib/lxc/$LXC_NAME/rootfs - - 'sudo lxc-create -n $LXC_NAME -t ubuntu -- -r $LXC_RELEASE --mirror http://mirrors.us.kernel.org/ubuntu --packages python || true' - - 'sudo lxc-start -n $LXC_NAME && until (sudo lxc-info -n $LXC_NAME | grep -q ^IP:); do printf . && sleep 1; done && sleep 2' - - export LXC_IP="$(sudo lxc-info -Hin $LXC_NAME)" - - sudo /bin/bash -c "printf '\n$LXC_IP test.lxc\n' >> /etc/hosts" - ssh-keygen -f ~/.ssh/id_rsa -t rsa -N '' - chmod 0644 ~/.ssh/config - - sudo mkdir -vm 0700 $LXC_ROOTFS/root/.ssh/ - - sudo cp -v ~/.ssh/id_rsa.pub $LXC_ROOTFS/root/.ssh/authorized_keys - - sudo apt-get install build-essential libssl-dev libffi-dev python-dev + - echo -e "#cloud-config\nssh_authorized_keys:\n - $(cat ~/.ssh/id_rsa.pub)" | sudo lxc profile set default user.user-data - + - sudo cp -f tests/lxd-bridge /etc/default/lxd-bridge + - sudo service lxd restart + - sudo lxc launch ${LXC_DISTRO}:${LXC_RELEASE} ${LXC_NAME} + - until host ${LXC_NAME}.lxd 10.0.8.1 -t A; do sleep 3; done + - export LXC_IP="$(dig ${LXC_NAME}.lxd @10.0.8.1 +short)" - pip install -r requirements.txt - pip install ansible-lint - gem install awesome_bot diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index 5cb7c3f2..c151488f 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,7 +2,7 @@ set -ex -DEPLOY_ARGS="server_ip=$LXC_IP server_user=root IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false" +DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false" if [ "${LXC_NAME}" == "docker" ] then diff --git a/tests/lxd-bridge b/tests/lxd-bridge new file mode 100644 index 00000000..0614e87b --- /dev/null +++ b/tests/lxd-bridge @@ -0,0 +1,16 @@ +USE_LXD_BRIDGE="true" +LXD_BRIDGE="lxdbr0" +UPDATE_PROFILE="true" +LXD_CONFILE="" +LXD_DOMAIN="lxd" +LXD_IPV4_ADDR="10.0.8.1" +LXD_IPV4_NETMASK="255.255.255.0" +LXD_IPV4_NETWORK="10.0.8.0/24" +LXD_IPV4_DHCP_RANGE="10.0.8.2,10.0.8.254" +LXD_IPV4_DHCP_MAX="250" +LXD_IPV4_NAT="true" +LXD_IPV6_ADDR="" +LXD_IPV6_MASK="" +LXD_IPV6_NETWORK="" +LXD_IPV6_NAT="false" +LXD_IPV6_PROXY="true" diff --git a/tests/update-users.sh b/tests/update-users.sh index df7066d1..8122a156 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -3,7 +3,7 @@ set -ex CAPW=`cat /tmp/ca_password` -USER_ARGS="server_ip=$LXC_IP server_user=root ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" +USER_ARGS="server_ip=$LXC_IP server_user=ubuntu ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" sed -i 's/- jack$/- jack_test/' config.cfg From 0de0952cf00a518ec5424ca966aeaf576260b15e Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Fri, 18 May 2018 12:35:56 +0300 Subject: [PATCH 622/769] fix requirements.txt SecretStorage version (#914) Related to issue #877. Latest SecretStorage build requires Python '>=3.5' but Algo is running on Python 2 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e7443ab0..dae2ab65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ setuptools>=11.3 +SecretStorage < 3 ansible[azure]==2.4.3 dopy==0.3.5 boto>=2.5 From 9fdbfb0977147beba8fc681a46413e22018b2615 Mon Sep 17 00:00:00 2001 From: Stijn Balk Date: Wed, 23 May 2018 19:17:10 +0300 Subject: [PATCH 623/769] Update GCP regions (#957) * Update GCP regions according to https://cloud.google.com/compute/docs/regions-zones/ * Update GCP regions according to https://cloud.google.com/compute/docs/regions-zones/ * set default back to belgium B --- algo | 170 +++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 95 insertions(+), 75 deletions(-) diff --git a/algo b/algo index 7b4d4377..8091789d 100755 --- a/algo +++ b/algo @@ -425,84 +425,104 @@ Name the vpn server: read -p " What zone should the server be located in? - 1. Western US (Oregon A) - 2. Western US (Oregon B) - 3. Western US (Oregon C) - 4. Central US (Iowa A) - 5. Central US (Iowa B) - 6. Central US (Iowa C) - 7. Central US (Iowa F) - 8. Eastern US (Northern Virginia A) - 9. Eastern US (Northern Virginia B) - 10. Eastern US (Northern Virginia C) - 11. Eastern US (South Carolina B) - 12. Eastern US (South Carolina C) - 13. Eastern US (South Carolina D) - 14. Western Europe (Belgium B) - 15. Western Europe (Belgium C) - 16. Western Europe (Belgium D) - 17. Western Europe (London A) - 18. Western Europe (London B) - 19. Western Europe (London C) - 20. Western Europe (Frankfurt A) - 21. Western Europe (Frankfurt B) - 22. Western Europe (Frankfurt C) - 23. Southeast Asia (Singapore A) - 24. Southeast Asia (Singapore B) - 25. East Asia (Taiwan A) - 26. East Asia (Taiwan B) - 27. East Asia (Taiwan C) - 28. Northeast Asia (Tokyo A) - 29. Northeast Asia (Tokyo B) - 30. Northeast Asia (Tokyo C) - 31. Australia (Sydney A) - 32. Australia (Sydney B) - 33. Australia (Sydney C) - 34. South America (São Paulo A) - 35. South America (São Paulo B) - 36. South America (São Paulo C) + 1. Eastern Canada (Montreal A) + 2. Eastern Canada (Montreal B) + 3. Eastern Canada (Montreal C) + 4. Central US (Iowa A) + 5. Central US (Iowa B) + 6. Central US (Iowa C) + 7. Central US (Iowa F) + 8. Western US (Oregon A) + 9. Western US (Oregon B) + 10. Western US (Oregon C) + 11. Eastern US (Northern Virginia A) + 12. Eastern US (Northern Virginia B) + 13. Eastern US (Northern Virginia C) + 14. Eastern US (South Carolina B) + 15. Eastern US (South Carolina C) + 16. Eastern US (South Carolina D) + 17. South America East (São Paulo A) + 18. South America East (São Paulo B) + 19. South America East (São Paulo C) + 20. Western Europe (Belgium B) + 21. Western Europe (Belgium C) + 22. Western Europe (Belgium D) + 23. Western Europe (London A) + 24. Western Europe (London B) + 25. Western Europe (London C) + 26. Western Europe (Frankfurt A) + 27. Western Europe (Frankfurt B) + 28. Western Europe (Frankfurt C) + 29. Western Europe (Netherlands A) + 30. Western Europe (Netherlands B) + 31. Western Europe (Netherlands C) + 32. South Asia (Mumbai A) + 33. South Asia (Mumbai B) + 34. South Asia (Mumbai C) + 35. Southeast Asia (Singapore A) + 36. Southeast Asia (Singapore B) + 37. Southeast Asia (Singapore C) + 38. East Asia (Taiwan A) + 39. East Asia (Taiwan B) + 40. East Asia (Taiwan C) + 41. Northeast Asia (Tokyo A) + 42. Northeast Asia (Tokyo B) + 43. Northeast Asia (Tokyo C) + 44. Australia (Sydney A) + 45. Australia (Sydney B) + 46. Australia (Sydney C) -Please choose the number of your zone. Press enter for default (#14) zone. -[14]: " -r region - region=${region:-14} +Please choose the number of your zone. Press enter for default (#20) zone. +[20]: " -r region + region=${region:-20} case "$region" in - 1) zone="us-west1-a" ;; - 2) zone="us-west1-b" ;; - 3) zone="us-west1-c" ;; - 4) zone="us-central1-a" ;; - 5) zone="us-central1-b" ;; - 6) zone="us-central1-c" ;; - 7) zone="us-central1-f" ;; - 8) zone="us-east4-a" ;; - 9) zone="us-east4-b" ;; - 10) zone="us-east4-c" ;; - 11) zone="us-east1-b" ;; - 12) zone="us-east1-c" ;; - 13) zone="us-east1-d" ;; - 14) zone="europe-west1-b" ;; - 15) zone="europe-west1-c" ;; - 16) zone="europe-west1-d" ;; - 17) zone="europe-west2-a" ;; - 18) zone="europe-west2-b" ;; - 19) zone="europe-west2-c" ;; - 20) zone="europe-west3-a" ;; - 21) zone="europe-west3-b" ;; - 22) zone="europe-west3-c" ;; - 23) zone="asia-southeast1-a" ;; - 24) zone="asia-southeast1-b" ;; - 25) zone="asia-east1-a" ;; - 26) zone="asia-east1-b" ;; - 27) zone="asia-east1-c" ;; - 28) zone="asia-northeast1-a" ;; - 29) zone="asia-northeast1-b" ;; - 30) zone="asia-northeast1-c" ;; - 31) zone="australia-southeast1-a" ;; - 32) zone="australia-southeast1-b" ;; - 33) zone="australia-southeast1-c" ;; - 34) zone="southamerica-east1-a" ;; - 35) zone="southamerica-east1-b" ;; - 36) zone="southamerica-east1-c" ;; + 1) zone="northamerica-northeast1-a" ;; + 2) zone="northamerica-northeast1-b" ;; + 3) zone="northamerica-northeast1-c" ;; + 4) zone="us-central1-a" ;; + 5) zone="us-central1-b" ;; + 6) zone="us-central1-c" ;; + 7) zone="us-central1-f" ;; + 8) zone="us-west1-a" ;; + 9) zone="us-west1-b" ;; + 10) zone="us-west1-c" ;; + 11) zone="us-east4-a" ;; + 12) zone="us-east4-b" ;; + 13) zone="us-east4-c" ;; + 14) zone="us-east1-b" ;; + 15) zone="us-east1-c" ;; + 16) zone="us-east1-d" ;; + 17) zone="southamerica-east1-a" ;; + 18) zone="southamerica-east1-b" ;; + 19) zone="southamerica-east1-c" ;; + 20) zone="europe-west1-b" ;; + 21) zone="europe-west1-c" ;; + 22) zone="europe-west1-d" ;; + 23) zone="europe-west2-a" ;; + 24) zone="europe-west2-b" ;; + 25) zone="europe-west2-c" ;; + 26) zone="europe-west3-a" ;; + 27) zone="europe-west3-b" ;; + 28) zone="europe-west3-c" ;; + 29) zone="europe-west4-a" ;; + 30) zone="europe-west4-b" ;; + 31) zone="europe-west4-c" ;; + 32) zone="asia-south1-a" ;; + 33) zone="asia-south1-b" ;; + 34) zone="asia-south1-c" ;; + 35) zone="asia-southeast1-a" ;; + 36) zone="asia-southeast1-b" ;; + 37) zone="asia-southeast1-c" ;; + 38) zone="asia-east1-a" ;; + 39) zone="asia-east1-b" ;; + 40) zone="asia-east1-c" ;; + 41) zone="asia-northeast1-a" ;; + 42) zone="asia-northeast1-b" ;; + 43) zone="asia-northeast1-c" ;; + 44) zone="australia-southeast1-a" ;; + 45) zone="australia-southeast1-b" ;; + 46) zone="australia-southeast1-c" ;; esac ROLES="gce vpn cloud" From 87836e0358c3ae7e408aaae1b6fb11b7a4850064 Mon Sep 17 00:00:00 2001 From: Evgeny Aleksandrov Date: Thu, 24 May 2018 09:00:38 +0300 Subject: [PATCH 624/769] Fix typo (#960) --- .../tasks/{ipec_configuration.yml => ipsec_configuration.yml} | 0 roles/vpn/tasks/main.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename roles/vpn/tasks/{ipec_configuration.yml => ipsec_configuration.yml} (100%) diff --git a/roles/vpn/tasks/ipec_configuration.yml b/roles/vpn/tasks/ipsec_configuration.yml similarity index 100% rename from roles/vpn/tasks/ipec_configuration.yml rename to roles/vpn/tasks/ipsec_configuration.yml diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index e0d0d1bf..003c4761 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -15,7 +15,7 @@ - name: Install strongSwan package: name=strongswan state=present - - include_tasks: ipec_configuration.yml + - include_tasks: ipsec_configuration.yml - include_tasks: openssl.yml tags: update-users - include_tasks: distribute_keys.yml From d9dc68164f80520e263f8cca6c138f58e590c1a0 Mon Sep 17 00:00:00 2001 From: Evgeny Aleksandrov Date: Thu, 24 May 2018 09:01:26 +0300 Subject: [PATCH 625/769] Remove algo_params (#961) --- roles/vpn/tasks/openssl.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 8dbd4ef8..053470fb 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -44,7 +44,7 @@ shell: > {{ openssl_bin }} ecparam -name prime256v1 -out ecparams/prime256v1.pem && {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -newkey ec:ecparams/prime256v1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 @@ -71,7 +71,7 @@ - name: Build the server pair shell: > {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -newkey ec:ecparams/prime256v1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes @@ -93,7 +93,7 @@ - name: Build the client's pair shell: > {{ openssl_bin }} req -utf8 -new - -newkey {{ algo_params | default('ec:ecparams/prime256v1.pem') }} + -newkey ec:ecparams/prime256v1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes From d27b849f24a4d69531601ec09692bc080993546a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 24 May 2018 17:08:14 +0300 Subject: [PATCH 626/769] Ubuntu1804 (#925) - Fixes #897 #944 #956 Work in progress. Lightsail is not ready for Ubuntu 18.04 yet - [x] DigitalOcean ~~- [ ] Amazon Lightsail~~ - [x] Amazon EC2 - [x] Microsoft Azure - [x] Google Compute Engine - [x] Scaleway - [x] OpenStack (DreamCompute optimised) --- .travis.yml | 7 ++-- algo | 34 +++++++++---------- config.cfg | 14 ++++---- deploy.yml | 1 - playbooks/common.yml | 2 +- playbooks/ubuntu.yml | 7 +++- roles/cloud-ec2/files/stack.yml | 18 ++-------- roles/cloud-scaleway/tasks/image_facts.yml | 9 +++++ roles/cloud-scaleway/tasks/main.yml | 24 ++++++++----- roles/common/handlers/main.yml | 7 ++-- roles/common/tasks/ubuntu.yml | 33 ++++++------------ .../common/templates/10-algo-lo100.network.j2 | 7 ++++ .../templates/10-loopback-services.cfg.j2 | 9 ----- roles/dns_encryption/tasks/ubuntu.yml | 20 +++++------ 14 files changed, 91 insertions(+), 101 deletions(-) create mode 100644 roles/cloud-scaleway/tasks/image_facts.yml create mode 100644 roles/common/templates/10-algo-lo100.network.j2 delete mode 100644 roles/common/templates/10-loopback-services.cfg.j2 diff --git a/.travis.yml b/.travis.yml index e3ccf43a..b06bf3b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,13 +35,12 @@ cache: before_cache: - mkdir $HOME/lxc - - sudo tar cf $HOME/lxc/cache.tar /var/cache/lxc/ + - sudo tar cf $HOME/lxc/cache.tar /var/lib/lxd/images/ - sudo chown $USER. $HOME/lxc/cache.tar env: - - LXC_NAME=ubuntu1604 LXC_DISTRO=ubuntu LXC_RELEASE=xenial - - LXC_NAME=ubuntu1710 LXC_DISTRO=ubuntu LXC_RELEASE=artful - - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=artful + - LXC_NAME=ubuntu1804 LXC_DISTRO=ubuntu LXC_RELEASE=18.04 + - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=18.04 before_install: - test "${LXC_NAME}" != "docker" || docker build -t travis/algo . diff --git a/algo b/algo index 8091789d..73e39657 100755 --- a/algo +++ b/algo @@ -211,7 +211,7 @@ Name the vpn server: 10. Singapore 11. Toronto 12. Bangalore - + Enter the number of your desired region: [7]: " -r region region=${region:-7} @@ -273,7 +273,7 @@ Name the vpn server: 14. ap-southeast-2 Asia Pacific (Sydney) 15. ap-south-1 Asia Pacific (Mumbai) 16. sa-east-1 South America (São Paulo) - + Enter the number of your desired region: [1]: " -r aws_region aws_region=${aws_region:-1} @@ -335,7 +335,7 @@ Name the vpn server: 10. eu-central-1 EU (Frankfurt) 11. eu-west-1 EU (Ireland) 12. eu-west-2 EU (London) - + Enter the number of your desired region: [1]: " -r algo_region algo_region=${algo_region:-1} @@ -471,7 +471,7 @@ Name the vpn server: 44. Australia (Sydney A) 45. Australia (Sydney B) 46. Australia (Sydney C) - + Please choose the number of your zone. Press enter for default (#20) zone. [20]: " -r region region=${region:-20} @@ -575,13 +575,12 @@ algo_provisioning () { echo -n " What provider would you like to use? 1. DigitalOcean - 2. Amazon Lightsail - 3. Amazon EC2 - 4. Microsoft Azure - 5. Google Compute Engine - 6. Scaleway - 7. OpenStack (DreamCompute optimised) - 8. Install to existing Ubuntu 16.04 server (Advanced) + 2. Amazon EC2 + 3. Microsoft Azure + 4. Google Compute Engine + 5. Scaleway + 6. OpenStack (DreamCompute optimised) + 7. Install to existing Ubuntu 16.04 server (Advanced) Enter the number of your desired provider : " @@ -590,13 +589,12 @@ Enter the number of your desired provider case "$N" in 1) digitalocean; ;; - 2) lightsail; ;; - 3) ec2; ;; - 4) azure; ;; - 5) gce; ;; - 6) scaleway; ;; - 7) openstack; ;; - 8) non_cloud; ;; + 2) ec2; ;; + 3) azure; ;; + 4) gce; ;; + 5) scaleway; ;; + 6) openstack; ;; + 7) non_cloud; ;; *) exit 1 ;; esac diff --git a/config.cfg b/config.cfg index 02a9ec54..e71e7d01 100644 --- a/config.cfg +++ b/config.cfg @@ -80,29 +80,29 @@ cloud_providers: image: offer: UbuntuServer publisher: Canonical - sku: '16.04-LTS' # 16.04-LTS / 17.04 + sku: '18.04-LTS' version: latest digitalocean: size: s-1vcpu-1gb - image: "ubuntu-16-04-x64" # ubuntu-16-04-x64 / ubuntu-17-10-x64 + image: "ubuntu-18-04-x64" ec2: size: t2.micro image: - name: "ubuntu-xenial-16.04" # ubuntu-xenial-16.04 / ubuntu-zesty-17.04 + name: "ubuntu-bionic-18.04" owner: "099720109477" gce: size: f1-micro - image: ubuntu-1604 # ubuntu-1604 / ubuntu-1704 + image: ubuntu-1804 lightsail: size: nano_1_0 image: ubuntu_16_04 scaleway: - size: VC1S - image: Ubuntu Xenial + size: START1-S + image: Ubuntu Bionic Beaver arch: x86_64 openstack: flavor_ram: ">=512" - image: Ubuntu-16.04 + image: Ubuntu-18.04 local: fail_hint: diff --git a/deploy.yml b/deploy.yml index 5ee93809..e58f3c5a 100644 --- a/deploy.yml +++ b/deploy.yml @@ -26,7 +26,6 @@ - { role: cloud-ec2, tags: ['ec2'] } - { role: cloud-gce, tags: ['gce'] } - { role: cloud-azure, tags: ['azure'] } - - { role: cloud-lightsail, tags: ['lightsail'] } - { role: cloud-scaleway, tags: ['scaleway'] } - { role: cloud-openstack, tags: ['openstack'] } - { role: local, tags: ['local'] } diff --git a/playbooks/common.yml b/playbooks/common.yml index 5628c37f..e0aea2bb 100644 --- a/playbooks/common.yml +++ b/playbooks/common.yml @@ -6,7 +6,7 @@ - name: Ubuntu pre-tasks include_tasks: ubuntu.yml - when: '"Ubuntu" in OS.stdout' + when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' - name: FreeBSD pre-tasks include_tasks: freebsd.yml diff --git a/playbooks/ubuntu.yml b/playbooks/ubuntu.yml index d67cbde4..bf7ac5b5 100644 --- a/playbooks/ubuntu.yml +++ b/playbooks/ubuntu.yml @@ -1,7 +1,12 @@ --- - name: Ubuntu | Install prerequisites - raw: sleep 10 && sudo apt-get update -qq && sudo apt-get install -qq -y python2.7 + raw: "{{ item }}" + with_items: + - sleep 10 + - apt-get update -qq + - apt-get install -qq -y python2.7 sudo + become: true - name: Ubuntu | Configure defaults raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 diff --git a/roles/cloud-ec2/files/stack.yml b/roles/cloud-ec2/files/stack.yml index 7f814e35..5a8abf52 100644 --- a/roles/cloud-ec2/files/stack.yml +++ b/roles/cloud-ec2/files/stack.yml @@ -147,11 +147,6 @@ Resources: Metadata: AWS::CloudFormation::Init: config: - users: - ubuntu: - groups: - - "sudo" - homeDir: "/home/ubuntu/" files: /home/ubuntu/.ssh/authorized_keys: content: @@ -173,18 +168,9 @@ Resources: "Fn::Base64": !Sub | #!/bin/bash -xe - # http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-migrate-ipv6.html - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1013597 - cat < /etc/network/interfaces.d/60-default-with-ipv6.cfg - iface eth0 inet6 dhcp - up sysctl net.ipv6.conf.\$IFACE.accept_ra=2 - pre-down ip link set dev \$IFACE up - EOF - ifdown eth0; ifup eth0 - dhclient -6 apt-get update - apt-get -y install python-setuptools - easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz + apt-get -y install python-pip + pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} Tags: diff --git a/roles/cloud-scaleway/tasks/image_facts.yml b/roles/cloud-scaleway/tasks/image_facts.yml new file mode 100644 index 00000000..1faa3d33 --- /dev/null +++ b/roles/cloud-scaleway/tasks/image_facts.yml @@ -0,0 +1,9 @@ +--- +- name: Set image id as a fact + set_fact: + image_id: "{{ item.id }}" + no_log: true + when: + - cloud_providers.scaleway.image == item.name + - cloud_providers.scaleway.arch == item.arch + with_items: "{{ outer_item['json']['images'] }}" diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 805b4de4..7664d278 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -35,7 +35,7 @@ when: scaleway_organization == item.name with_items: "{{ scaleway_organizations.json.organizations }}" - - name: Get images + - name: Get total count of images uri: url: "https://cp-{{ algo_region }}.scaleway.com/images" method: GET @@ -43,16 +43,24 @@ Content-Type: 'application/json' X-Auth-Token: "{{ scaleway_auth_token }}" status_code: 200 + register: scaleway_pages + + - name: Get images + uri: + url: "https://cp-{{ algo_region }}.scaleway.com/images?per_page=100&page={{ item }}" + method: GET + headers: + Content-Type: 'application/json' + X-Auth-Token: "{{ scaleway_auth_token }}" + status_code: 200 register: scaleway_images + with_sequence: start=1 end={{ ((scaleway_pages.x_total_count|int / 100)| round )|int }} - name: Set image id as a fact - set_fact: - image_id: "{{ item.id }}" - no_log: true - when: - - cloud_providers.scaleway.image in item.name - - cloud_providers.scaleway.arch == item.arch - with_items: "{{ scaleway_images.json.images }}" + include_tasks: image_facts.yml + with_items: "{{ scaleway_images['results'] }}" + loop_control: + loop_var: outer_item - name: Create a server uri: diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 2272403c..1415245e 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -7,8 +7,11 @@ - name: flush routing cache shell: echo 1 > /proc/sys/net/ipv4/route/flush -- name: restart loopback - shell: ifdown lo:100 && ifup lo:100 +- name: restart systemd-networkd + systemd: + name: systemd-networkd + state: restarted + daemon_reload: true - name: restart loopback bsd shell: > diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 8b09374c..e1d97149 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -48,34 +48,21 @@ tags: - cloud -- name: Install system specific tools - package: name="{{ item }}" state=present - with_items: - - ifupdown - tags: - - always - -- name: Ensure the interfaces directory exists - file: - path: /etc/network/interfaces.d/ - state: directory - mode: 0755 - owner: root - group: root - tags: - - always - - name: Loopback for services configured - template: src=10-loopback-services.cfg.j2 dest=/etc/network/interfaces.d/10-loopback-services.cfg + template: + src: 10-algo-lo100.network.j2 + dest: /etc/systemd/network/10-algo-lo100.network notify: - - restart loopback + - restart systemd-networkd tags: - always -- name: Loopback included into the network config - lineinfile: dest=/etc/network/interfaces line='source /etc/network/interfaces.d/10-loopback-services.cfg' state=present - notify: - - restart loopback +- name: systemd-networkd enabled and started + systemd: + name: systemd-networkd + state: started + enabled: true + daemon_reload: true tags: - always diff --git a/roles/common/templates/10-algo-lo100.network.j2 b/roles/common/templates/10-algo-lo100.network.j2 new file mode 100644 index 00000000..257396c6 --- /dev/null +++ b/roles/common/templates/10-algo-lo100.network.j2 @@ -0,0 +1,7 @@ +[Match] +Name=lo + +[Network] +Label=lo:100 +Address={{ local_service_ip }}/32 +Address=FCAA::1/64 diff --git a/roles/common/templates/10-loopback-services.cfg.j2 b/roles/common/templates/10-loopback-services.cfg.j2 deleted file mode 100644 index 09f572de..00000000 --- a/roles/common/templates/10-loopback-services.cfg.j2 +++ /dev/null @@ -1,9 +0,0 @@ -auto lo:100 -iface lo:100 inet static - address {{ local_service_ip }} - netmask 255.255.255.255 - -iface lo:100 inet6 static - address FCAA::1 - netmask 64 - autoconf 0 diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index a543f842..9290cf43 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -35,14 +35,12 @@ owner: root group: root -#- name: Ubuntu | Setup the cgroup limitations for dnscrypt-proxy -# copy: -# dest: /etc/systemd/system/dnscrypt-proxy.service.d/100-CustomLimitations.conf -# content: | -# [Service] -# MemoryLimit=16777216 -# CPUAccounting=true -# CPUQuota=5% -# notify: -# - daemon-reload -# - restart dnscrypt-proxy +- name: Ubuntu | Add capabilities to bind ports + copy: + dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-capabilities.conf + content: | + [Service] + AmbientCapabilities=CAP_NET_BIND_SERVICE + notify: + - daemon-reload + - restart dnscrypt-proxy From 3488e660ad3535332f59ba5c613d0e1b558e630b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 24 May 2018 18:15:27 +0300 Subject: [PATCH 627/769] Add WireGuard support for Android (#910) * WireGuard Implementation * Update client-android.md * Update README.md * WireGuard unattended upgrades * Update README.md * reload-module-on-update and syntax fix * SaveConfig to true * Azure firewall. Fixes #962 * Update README.md * Update client-android.md --- .travis.yml | 9 ++- CHANGELOG.md | 10 ++++ README.md | 4 +- config.cfg | 2 + deploy.yml | 1 + docs/client-android.md | 48 +-------------- roles/cloud-azure/tasks/main.yml | 6 ++ roles/cloud-ec2/files/stack.yml | 6 ++ roles/cloud-ec2/tasks/cloudformation.yml | 1 + roles/cloud-gce/tasks/main.yml | 2 +- roles/cloud-lightsail/tasks/main.yml | 3 + roles/cloud-openstack/tasks/main.yml | 1 + roles/common/tasks/ubuntu.yml | 1 + .../common/templates/50unattended-upgrades.j2 | 3 + roles/vpn/tasks/client_configs.yml | 19 ------ roles/vpn/templates/android_html_helper.j2 | 1 - roles/vpn/templates/rules.v4.j2 | 14 +++-- roles/vpn/templates/rules.v6.j2 | 11 ++-- roles/vpn/templates/sswan.j2 | 15 ----- roles/wireguard/defaults/main.yml | 18 ++++++ roles/wireguard/handlers/main.yml | 5 ++ roles/wireguard/meta/main.yml | 3 + roles/wireguard/tasks/keys.yml | 60 +++++++++++++++++++ roles/wireguard/tasks/main.yml | 57 ++++++++++++++++++ roles/wireguard/templates/client.conf.j2 | 10 ++++ roles/wireguard/templates/server.conf.j2 | 18 ++++++ tests/local-deploy.sh | 2 +- 27 files changed, 235 insertions(+), 95 deletions(-) delete mode 100644 roles/vpn/templates/android_html_helper.j2 delete mode 100644 roles/vpn/templates/sswan.j2 create mode 100644 roles/wireguard/defaults/main.yml create mode 100644 roles/wireguard/handlers/main.yml create mode 100644 roles/wireguard/meta/main.yml create mode 100644 roles/wireguard/tasks/keys.yml create mode 100644 roles/wireguard/tasks/main.yml create mode 100644 roles/wireguard/templates/client.conf.j2 create mode 100644 roles/wireguard/templates/server.conf.j2 diff --git a/.travis.yml b/.travis.yml index b06bf3b6..2a2fa1d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ addons: apt: sources: - sourceline: 'ppa:ubuntu-lxc/stable' + - sourceline: 'ppa:wireguard/wireguard' packages: - python-pip - lxd @@ -27,6 +28,8 @@ addons: - libssl-dev - libffi-dev - python-dev + - linux-headers-$(uname -r) + - wireguard-dkms cache: directories: @@ -43,7 +46,7 @@ env: - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=18.04 before_install: - - test "${LXC_NAME}" != "docker" || docker build -t travis/algo . + - test "${LXC_NAME}" != "docker" && sudo modprobe wireguard || docker build -t travis/algo . install: - sudo tar xf $HOME/lxc/cache.tar -C / || echo "Didn't extract cache." @@ -63,8 +66,8 @@ install: script: # - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new -# - shellcheck algo -# - ansible-lint deploy.yml users.yml deploy_client.yml +# - shellcheck algo +# - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check - ./tests/local-deploy.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 78644798..5d3028a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 30 Apr 2018 +### Added +- WireGuard support + +### Removed +- Android StrongSwan profiles + +### Release notes +- StrongSwan profiles for Android are deprecated now. Use WireGuard + ## 25 Apr 2018 ### Added - DNScrypt-proxy added diff --git a/README.md b/README.md index 53619d10..61d61c54 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC ## Features -* Supports only IKEv2 with strong crypto: AES-GCM, SHA2, and P-256 +* Supports only IKEv2 with strong crypto (AES-GCM, SHA2, and P-256) and [WireGuard](https://www.wireguard.com/) * Generates Apple profiles to auto-configure iOS and macOS devices * Includes a helper script to add and remove users * Blocks ads with a local DNS resolver (optional) @@ -97,7 +97,7 @@ Certificates and configuration files that users will need are placed in the `con ### Android Devices -No version of Android supports IKEv2. Install the [strongSwan VPN Client for Android 4 and newer](https://play.google.com/store/apps/details?id=org.strongswan.android). Import the corresponding user.p12 certificate to your device. See the [Android setup instructions](/docs/client-android.md) for more a more detailed walkthrough. +WireGuard is used to provide VPN services on Android. Install the [WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). Import the corresponding `wireguard/.conf` file to your device, then setup a new connection with it. See the [Android setup instructions](/docs/client-android.md) for more detailed walkthrough. ### Windows 10 diff --git a/config.cfg b/config.cfg index e71e7d01..731c71de 100644 --- a/config.cfg +++ b/config.cfg @@ -15,6 +15,8 @@ easyrsa_reinit_existent: False vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' +wireguard_enabled: true +wireguard_port: 51820 server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" diff --git a/deploy.yml b/deploy.yml index e58f3c5a..532820c7 100644 --- a/deploy.yml +++ b/deploy.yml @@ -64,6 +64,7 @@ roles: - { role: dns_adblocking, tags: [ 'dns', 'adblock' ] } - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } + - { role: wireguard, tags: [ 'vpn', 'wireguard' ], when: wireguard_enabled } - { role: vpn, tags: [ 'vpn' ] } post_tasks: diff --git a/docs/client-android.md b/docs/client-android.md index 1175da79..1e98f6d7 100644 --- a/docs/client-android.md +++ b/docs/client-android.md @@ -2,48 +2,6 @@ ## Installation via profiles -1. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android). -2. Copy `android_{username}.sswan` and `android_{username}_helper.html` to your phone's internal storage. -3. Open the StrongSwan app and go to 'Import VPN profile'. -4. Select the `android_{username}.sswan` file to configure the VPN with your profile. - -## Manual installation - -**NOTE:** If you are a Project Fi user, you must disable WiFi Assistant before continuing. See the [strongSwan documentation](https://wiki.strongswan.org/projects/strongswan/wiki/AndroidVPNClient) for details. - -| Instruction | Screenshot(s) | -| ----------- | ---------- | -| 1. Copy your `{username}.p12` certificate to your phone's internal storage. | | -| 2. [Install the strongSwan VPN Client](https://play.google.com/store/apps/details?id=org.strongswan.android) (Android 4+) | | -| 3. Open the app and tap "ADD VPN PROFILE" in the top right. | [![step3-thumb]][step3-screen] | -| 4. Enter the IP address or hostname of your Algo server and set the "VPN Type" to "IKEv2 Certificate". | [![step4-thumb]][step4-screen] | -| 5. Tap "Select user certificate". You will be shown a prompt, tap "INSTALL". | [![step5-thumb]][step5-screen] | -| 6. Use the "Open from" menu to select your certificate. If you downloaded your certificate to your phone, you may find that using the "Downloads" shortcut results in your `{username}.p12` certificate being grayed out. If this happens go back to the "Open from" menu and tap on the name of your phone. This will bring up the filesystem. From here, navigate to the folder where you saved your cert (such as "Downloads"), and try again. | [![step6-thumb]][step6-screen] | -| 7. Enter the password for your certificate. This password was printed to your console at the end of running the `algo` deployment script. Please note that in some cases, extracting the certificate can take several minutes. | [![step7-thumb]][step7-screen] | -| 8. Give your certificate a name (it will default to your Algo username), and ensure that "Credential use" is set to "VPN and apps". Tap "OK". | [![step8-thumb]][step8-screen] | -| 9. You'll then be brought to another prompt. Ensure your newly imported certificate is selected, and tap "ALLOW". Then, tap "SAVE" in the top right. | [![step9-thumb]][step9-screen] | -| 10. You will be returned to the main menu, and your newly-configured VPN profile should be listed. Tap the profile to connect. | [![step10-thumb]][step10-screen] | - -## Troubleshooting -### Tapping the VPN profile in strongSwan has no effect. -Ensure that "WiFi Assistant" and any other always-on VPNs are disabled before attempting to enable a strongSwan VPN. If any other VPN is active, strongSwan may silently fail to initialize a VPN connection. On Android 7, your can manage your VPNs by going to: Settings > Tap "More" under "Wireless & networks" > VPN > tap the gear icon next to any non-strongSwan VPNs listed and ensure they are disabled. - - -[step3-thumb]: https://i.imgur.com/LPwIGJE.png -[step4-thumb]: https://i.imgur.com/sFkDILg.png -[step5-thumb]: https://i.imgur.com/IliT5oD.png -[step6-thumb]: https://i.imgur.com/oghdCVp.png -[step7-thumb]: https://i.imgur.com/nDzJ7KS.png -[step8-thumb]: https://i.imgur.com/RPXSpCo.png -[step9-thumb]: https://i.imgur.com/uMinDPe.png -[step10-thumb]: https://i.imgur.com/hUEDjdo.png - - -[step3-screen]: https://i.imgur.com/xNMihCd.png -[step4-screen]: https://i.imgur.com/xYjoNNO.png -[step5-screen]: https://i.imgur.com/4qhKT1Z.png -[step6-screen]: https://i.imgur.com/MAaQuxH.png -[step7-screen]: https://i.imgur.com/aT2MPih.png -[step8-screen]: https://i.imgur.com/gvaKzkh.png -[step9-screen]: https://i.imgur.com/eZp8DNb.png -[step10-screen]: https://i.imgur.com/Nd8rYMJ.png +1. [Install the WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). +2. Copy `wireguard/{username}.conf` to your phone's internal storage. +3. Open the WireGuard app and add a connection using your AlgoVPN configuration file. diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index bee7e982..6a6e9de4 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -58,6 +58,12 @@ access: Allow priority: 120 direction: Inbound + - name: AllowWireGuard + protocol: Udp + destination_port_range: "{{ wireguard_port }}" + access: Allow + priority: 130 + direction: Inbound - name: Create a subnet azure_rm_subnet: diff --git a/roles/cloud-ec2/files/stack.yml b/roles/cloud-ec2/files/stack.yml index 5a8abf52..3660613b 100644 --- a/roles/cloud-ec2/files/stack.yml +++ b/roles/cloud-ec2/files/stack.yml @@ -9,6 +9,8 @@ Parameters: Type: String ImageIdParameter: Type: String + WireGuardPort: + Type: String Resources: VPC: Type: AWS::EC2::VPC @@ -132,6 +134,10 @@ Resources: FromPort: '4500' ToPort: '4500' CidrIp: 0.0.0.0/0 + - IpProtocol: udp + FromPort: !Ref WireGuardPort + ToPort: !Ref WireGuardPort + CidrIp: 0.0.0.0/0 Tags: - Key: Name Value: Algo diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml index 032a59b6..7c6fe374 100644 --- a/roles/cloud-ec2/tasks/cloudformation.yml +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -11,6 +11,7 @@ InstanceTypeParameter: "{{ cloud_providers.ec2.size }}" PublicSSHKeyParameter: "{{ lookup('file', SSH_keys.public) }}" ImageIdParameter: "{{ ami_image }}" + WireGuardPort: "{{ wireguard_port }}" tags: Environment: Algo register: stack diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index dafa7553..24a825cf 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -15,7 +15,7 @@ gce_net: name: "algo-net-{{ server_name }}" fwname: "algo-net-{{ server_name }}-fw" - allowed: "udp:500,4500;tcp:22" + allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" state: "present" mode: auto src_range: 0.0.0.0/0 diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index 437e8448..31f73e6f 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -22,6 +22,9 @@ - from_port: 500 to_port: 500 protocol: udp + - from_port: "{{ wireguard_port }}" + to_port: "{{ wireguard_port }}" + protocol: udp user_data: | #!/bin/bash mkdir -p /home/ubuntu/.ssh/ diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index 63dbb726..d470e89e 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -20,6 +20,7 @@ - { proto: icmp, port_min: -1, port_max: -1, range: 0.0.0.0/0 } - { proto: udp, port_min: 4500, port_max: 4500, range: 0.0.0.0/0 } - { proto: udp, port_min: 500, port_max: 500, range: 0.0.0.0/0 } + - { proto: udp, port_min: "{{ wireguard_port }}", port_max: "{{ wireguard_port }}", range: 0.0.0.0/0 } - name: Keypair created os_keypair: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index e1d97149..e5d9165a 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -89,6 +89,7 @@ - iptables-persistent - cgroup-tools - openssl + - resolvconf sysctl: - item: net.ipv4.ip_forward value: 1 diff --git a/roles/common/templates/50unattended-upgrades.j2 b/roles/common/templates/50unattended-upgrades.j2 index 0c55b702..a902c7ad 100644 --- a/roles/common/templates/50unattended-upgrades.j2 +++ b/roles/common/templates/50unattended-upgrades.j2 @@ -2,6 +2,9 @@ Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; "${distro_id}:${distro_codename}-updates"; +{% if wireguard_enabled %} + "LP-PPA-wireguard-wireguard:${distro_codename}"; +{% endif %} // "${distro_id}:${distro_codename}-proposed"; // "${distro_id}:${distro_codename}-backports"; }; diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index 4c6cbe92..52dff83c 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -21,25 +21,6 @@ - "{{ PayloadContent.results }}" no_log: True -- name: Build the strongswan app android config - template: - src: sswan.j2 - dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}.sswan - mode: 0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - no_log: True - -- name: Build the android helper html - template: - src: android_html_helper.j2 - dest: configs/{{ IP_subject_alt_name }}/android_{{ item.0 }}_helper.html - mode: 0600 - with_together: - - "{{ users }}" - no_log: True - - name: Build the client ipsec config file template: src: client_ipsec.conf.j2 diff --git a/roles/vpn/templates/android_html_helper.j2 b/roles/vpn/templates/android_html_helper.j2 deleted file mode 100644 index d27528aa..00000000 --- a/roles/vpn/templates/android_html_helper.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ item.0 }} diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index c51568aa..fe2878d6 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -19,7 +19,7 @@ # - https://github.com/trailofbits/algo/issues/216 # - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu # - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan --A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +-A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} COMMIT @@ -35,7 +35,8 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network }} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -m policy --pol none --dir out -j MASQUERADE + COMMIT @@ -62,7 +63,7 @@ COMMIT # rate limit ICMP traffic per source -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT # Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +-A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT # Allow any traffic from the VPN @@ -78,7 +79,7 @@ COMMIT {% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} # Drop traffic between VPN clients --A FORWARD -s {{ vpn_network }} -d {{ vpn_network }} -j DROP +-A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j DROP {% endif %} # Forward any packet that's part of an established connection @@ -92,4 +93,9 @@ COMMIT # Forward any IPSEC traffic from the VPN network -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT +# Forward any traffic from the WireGuard VPN network +{% if wireguard_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network }} -m policy --pol none --dir in -j ACCEPT +{% endif %} + COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 82ca8e16..df0603a8 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -13,7 +13,7 @@ {% if max_mss is defined %} # MSS is the TCP Max Segment Size # See rules.v4 for a more complete explanation --A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +-A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} COMMIT @@ -28,7 +28,7 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network_ipv6 }} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -m policy --pol none --dir out -j MASQUERADE COMMIT @@ -63,7 +63,7 @@ COMMIT # rate limit ICMP traffic per source -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT # Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500 -j ACCEPT +-A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT @@ -85,7 +85,7 @@ COMMIT -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT {% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} --A FORWARD -s {{ vpn_network_ipv6 }} -d {{ vpn_network_ipv6 }} -j DROP +-A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j DROP {% endif %} -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP @@ -93,6 +93,9 @@ COMMIT -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +{% if wireguard_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT +{% endif %} # Use the ICMPV6-CHECK chain, described above -A ICMPV6-CHECK -p icmpv6 -m hl ! --hl-eq 255 --icmpv6-type router-solicitation -j ICMPV6-CHECK-LOG diff --git a/roles/vpn/templates/sswan.j2 b/roles/vpn/templates/sswan.j2 deleted file mode 100644 index 405d44a2..00000000 --- a/roles/vpn/templates/sswan.j2 +++ /dev/null @@ -1,15 +0,0 @@ -{ - "uuid": "{{ 600000 | random | to_uuid }}", - "name": "Algo {{ IP_subject_alt_name }}", - "type": "ikev2-cert", - "remote": { - "addr": "{{ IP_subject_alt_name }}", - "cert": "{{ PayloadContentCA }}" - }, - "local": { - "p12": "{{ item.1.stdout }}" - }, - "ike-proposal": "{{ ciphers.defaults.ike | replace('!', '') }}", - "esp-proposal": "{{ ciphers.defaults.esp | replace('!', '') }}", - "mtu": 1280 -} diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml new file mode 100644 index 00000000..d94f3ff6 --- /dev/null +++ b/roles/wireguard/defaults/main.yml @@ -0,0 +1,18 @@ +--- +wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" +wireguard_interface: wg0 +wireguard_network_ipv4: + subnet: 10.19.49.0 + prefix: 24 + gateway: 10.19.49.1 + clients_range: 10.19.49 + clients_start: 100 +wireguard_network_ipv6: + subnet: 'fd9d:bc11:4021::' + prefix: 48 + gateway: 'fd9d:bc11:4021::1' + clients_range: 'fd9d:bc11:4021::' + clients_start: 100 +wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" +wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" +easyrsa_reinit_existent: false diff --git a/roles/wireguard/handlers/main.yml b/roles/wireguard/handlers/main.yml new file mode 100644 index 00000000..1063f5e6 --- /dev/null +++ b/roles/wireguard/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart wireguard + service: + name: "wg-quick@{{ wireguard_interface }}" + state: restarted diff --git a/roles/wireguard/meta/main.yml b/roles/wireguard/meta/main.yml new file mode 100644 index 00000000..a766ccc1 --- /dev/null +++ b/roles/wireguard/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - { role: common, tags: common } diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml new file mode 100644 index 00000000..322f974f --- /dev/null +++ b/roles/wireguard/tasks/keys.yml @@ -0,0 +1,60 @@ +--- +- name: Delete the lock files + file: + dest: "/etc/wireguard/private_{{ item }}.lock" + state: absent + when: easyrsa_reinit_existent|bool == True + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- name: Generate private keys + command: wg genkey + register: wg_genkey + args: + creates: "/etc/wireguard/private_{{ item }}.lock" + executable: bash + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- block: + - name: Save private keys + copy: + dest: "{{ wireguard_config_path }}/private/{{ item['item'] }}" + content: "{{ item['stdout'] }}" + mode: "0600" + no_log: true + when: item.changed + with_items: "{{ wg_genkey['results'] }}" + delegate_to: localhost + become: false + + - name: Touch the lock file + file: + dest: "/etc/wireguard/private_{{ item }}.lock" + state: touch + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + when: wg_genkey.changed + +- name: Generate public keys + shell: echo "{{ lookup('file', wireguard_config_path + '/private/' + item) }}" | wg pubkey + register: wg_pubkey + changed_when: false + args: + executable: bash + with_items: + - "{{ users }}" + - "{{ IP_subject_alt_name }}" + +- name: Save public keys + copy: + dest: "{{ wireguard_config_path }}/public/{{ item['item'] }}" + content: "{{ item['stdout'] }}" + mode: "0600" + no_log: true + with_items: "{{ wg_pubkey['results'] }}" + delegate_to: localhost + become: false diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml new file mode 100644 index 00000000..017e2ac3 --- /dev/null +++ b/roles/wireguard/tasks/main.yml @@ -0,0 +1,57 @@ +--- +- name: WireGuard repository configured + apt_repository: + repo: ppa:wireguard/wireguard + state: present + +- name: WireGuard installed + apt: + name: wireguard + state: present + update_cache: true + +- name: Ensure the required directories exist + file: + dest: "{{ wireguard_config_path }}/{{ item }}" + state: directory + recurse: true + with_items: + - private + - public + delegate_to: localhost + become: false + +- name: Generate keys + import_tasks: keys.yml + tags: update-users + +- name: WireGuard configured + template: + src: server.conf.j2 + dest: "/etc/wireguard/{{ wireguard_interface }}.conf" + mode: "0600" + notify: restart wireguard + tags: update-users + +- name: WireGuard reload-module-on-update + file: + dest: /etc/wireguard/.reload-module-on-update + state: touch + +- name: WireGuard users config generated + template: + src: client.conf.j2 + dest: "{{ wireguard_config_path }}/{{ item.1 }}.conf" + mode: "0600" + with_indexed_items: "{{ users }}" + tags: update-users + delegate_to: localhost + become: false + +- name: WireGuard enabled and started + service: + name: "wg-quick@{{ wireguard_interface }}" + state: started + enabled: true + +- meta: flush_handlers diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 new file mode 100644 index 00000000..59e5d52d --- /dev/null +++ b/roles/wireguard/templates/client.conf.j2 @@ -0,0 +1,10 @@ +[Interface] +PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} +Address = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + item.0 + 1 }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + item.0 + 1 }}/{{ wireguard_network_ipv6['prefix'] }} +{% endif %} +DNS = {{ local_service_ip }} + +[Peer] +PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} +AllowedIPs = 0.0.0.0/0, ::/0 +Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 new file mode 100644 index 00000000..a90e3fdc --- /dev/null +++ b/roles/wireguard/templates/server.conf.j2 @@ -0,0 +1,18 @@ +[Interface] +Address = {{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }} +{% endif %} + +DNS = {{ local_service_ip }} +ListenPort = {{ wireguard_port }} +PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} +SaveConfig = true +Table = off + +{% for u in users %} + +[Peer] +# {{ u }} +PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} +AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + loop.index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + loop.index }}/128 +{% endif %} +{% endfor %} diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index c151488f..efd127cb 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -6,7 +6,7 @@ DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP lo if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor,wireguard" else ansible-playbook deploy.yml -t cloud,local,vpn,dns,dns_over_https,ssh_tunneling,tests -e "${DEPLOY_ARGS}" --skip-tags apparmor fi From b928e4ff06ec14bdf3e4f23a65b218a099c40493 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 25 May 2018 21:02:16 +0800 Subject: [PATCH 628/769] fix faq entry about cryptography build failure (#967) --- docs/troubleshooting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2c860a58..09f3fbf8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -75,7 +75,7 @@ You don't have a working compiler installed. You should install the XCode compil ### Error: "fatal error: 'openssl/opensslv.h' file not found" -On macOS, you tried to install pycrypto and encountered the following error: +On macOS, you tried to install `cryptography` and encountered the following error: ``` build/temp.macosx-10.12-intel-2.7/_openssl.c:434:10: fatal error: 'openssl/opensslv.h' file not found @@ -94,7 +94,7 @@ Command /usr/bin/python -c "import setuptools, tokenize;__file__='/private/tmp/p Storing debug log for failure in /Users/algore/Library/Logs/pip.log ``` -You are running an old version of `pip` that cannot build the `pycrypto` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. +You are running an old version of `pip` that cannot download the binary `cryptography` dependency. Upgrade to a new version of `pip` by running `sudo pip install -U pip`. ### Error: "TypeError: must be str, not bytes" From d56f50180bc1bd877ace14d8a75750f38b3328a2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 25 May 2018 20:37:13 +0300 Subject: [PATCH 629/769] Extra line and better DNS configuration for WireGuard (#968) - Adds an extra line after the if statement. Jinja2 trims such blocks by default in Ansible. Fixes #965 - More appropriate way to configure DNS servers - Removes `DNS` option from the wireguard server config - Fixes dnscrypt-proxy restart --- roles/common/tasks/ubuntu.yml | 1 - roles/dns_encryption/handlers/main.yml | 3 ++- roles/dns_encryption/tasks/ubuntu.yml | 1 - roles/wireguard/defaults/main.yml | 6 ++++++ roles/wireguard/templates/client.conf.j2 | 3 ++- roles/wireguard/templates/server.conf.j2 | 1 - 6 files changed, 10 insertions(+), 5 deletions(-) diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index e5d9165a..e1d97149 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -89,7 +89,6 @@ - iptables-persistent - cgroup-tools - openssl - - resolvconf sysctl: - item: net.ipv4.ip_forward value: 1 diff --git a/roles/dns_encryption/handlers/main.yml b/roles/dns_encryption/handlers/main.yml index c46912b9..7947ef11 100644 --- a/roles/dns_encryption/handlers/main.yml +++ b/roles/dns_encryption/handlers/main.yml @@ -4,6 +4,7 @@ daemon_reload: true - name: restart dnscrypt-proxy - service: + systemd: name: dnscrypt-proxy state: restarted + daemon_reload: true diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 9290cf43..0f1cffcf 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -42,5 +42,4 @@ [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE notify: - - daemon-reload - restart dnscrypt-proxy diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index d94f3ff6..0559c50b 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -16,3 +16,9 @@ wireguard_network_ipv6: wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" easyrsa_reinit_existent: false +wireguard_dns_servers: >- + {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} + {{ local_service_ip }} + {% else %} + {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + {% endif %} diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 59e5d52d..f75f0f43 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -2,7 +2,8 @@ PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} Address = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + item.0 + 1 }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + item.0 + 1 }}/{{ wireguard_network_ipv6['prefix'] }} {% endif %} -DNS = {{ local_service_ip }} + +DNS = {{ wireguard_dns_servers }} [Peer] PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index a90e3fdc..3f9f45dd 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -2,7 +2,6 @@ Address = {{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }} {% endif %} -DNS = {{ local_service_ip }} ListenPort = {{ wireguard_port }} PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} SaveConfig = true From 2d9a36d13aaf55719379a661aefb4561cabced6c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 28 May 2018 22:16:06 +0300 Subject: [PATCH 630/769] Scaleway: enable ipv6 and switch to local boot (#974) - Enables IPv6 on Scaleway - Adds local boot on scaleway - Fixes #966 --- roles/cloud-scaleway/tasks/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 7664d278..31cc3f99 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -74,6 +74,8 @@ name: "{{ algo_server_name }}" image: "{{ image_id }}" commercial_type: "{{cloud_providers.scaleway.size }}" + enable_ipv6: true + boot_type: local tags: - Environment:Algo - AUTHORIZED_KEY={{ lookup('file', SSH_keys.public)|regex_replace(' ', '_') }} From aee043977f5fc1e89e01b7a1bbc9396a89c63076 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 30 May 2018 07:43:06 +0300 Subject: [PATCH 631/769] explicit installation of linux headers (#975) --- roles/common/tasks/ubuntu.yml | 2 +- tests/local-deploy.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index e1d97149..b0f347d7 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -88,7 +88,7 @@ - coreutils - iptables-persistent - cgroup-tools - - openssl + - "openssl{% if install_headers|default(true)|bool %},linux-headers-{{ ansible_kernel }}{% endif %}" sysctl: - item: net.ipv4.ip_forward value: 1 diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index efd127cb..246132f8 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,7 +2,7 @@ set -ex -DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false" +DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false install_headers=false" if [ "${LXC_NAME}" == "docker" ] then From daca84b6405258a7b247626efe9c87078aac1c46 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Wed, 30 May 2018 17:11:32 +0300 Subject: [PATCH 632/769] Update references to 18.04 --- README.md | 6 +++--- docs/cloud-do.md | 16 ++++++++-------- docs/deploy-to-ubuntu.md | 4 ++-- docs/deploy-to-unsupported-cloud.md | 2 +- docs/index.md | 3 +-- docs/troubleshooting.md | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 61d61c54..6f0b42c1 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 16.04 LTS server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server ## Anti-features @@ -116,7 +116,7 @@ Network Manager does not support AES-GCM. In order to support Linux Desktop clie Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, user.crt (user certificate), and user.key (private key) files to your client device. These will require customization based on your exact use case. These files were originally generated with a point-to-point OpenWRT-based VPN in mind. -#### Ubuntu Server 16.04 example +#### Ubuntu Server 18.04 example 1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan 2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` @@ -195,7 +195,7 @@ After this process completes, the Algo VPN server will contains only the users l - Configure [DigitalOcean](docs/cloud-do.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - - Deploy to your own [Ubuntu 16.04](docs/deploy-to-ubuntu.md) server + - Deploy to your own [Ubuntu 18.04](docs/deploy-to-ubuntu.md) server - Deploy to an [unsupported cloud provider](docs/deploy-to-unsupported-cloud.md) * [FAQ](docs/faq.md) * [Troubleshooting](docs/troubleshooting.md) diff --git a/docs/cloud-do.md b/docs/cloud-do.md index 15c8e288..b8f84681 100644 --- a/docs/cloud-do.md +++ b/docs/cloud-do.md @@ -12,7 +12,7 @@ On the **Tokens/Keys** tab, select **Generate New Token**. A dialog will pop up. ![The new token dialog, showing a form requesting a name and confirmation on the scope for the new token.](/docs/images/do-new-token.png) -You will be returned to the **Tokens/Keys** tab, and your new key will be shown under the **Personal Access Tokens** header. +You will be returned to the **Tokens/Keys** tab, and your new key will be shown under the **Personal Access Tokens** header. ![The new token in the listing.](/docs/images/do-view-token.png) @@ -20,9 +20,9 @@ Copy or note down the hash that shows below the name you entered, as this will b ## Using DigitalOcean with Algo (command) -These steps are for people who run Algo using Docker or using the "algo" command. +These steps are for people who run Algo using Docker or using the "algo" command. -First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. +First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. ``` What provider would you like to use? @@ -33,7 +33,7 @@ First you will be asked which server type to setup. You would want to enter "1" 5. Google Compute Engine 6. Scaleway 7. OpenStack (DreamCompute optimised) - 8. Install to existing Ubuntu 16.04 server + 8. Install to existing Ubuntu 18.04 server Enter the number of your desired provider : 1 @@ -44,17 +44,17 @@ Next you will be asked for the API Token value. Paste the API Token value you co ``` Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): [pasted values will not be displayed] -: +: ``` You will be prompted for the server name to enter. Feel free to leave this as the default ("algo.local") if you are not certain how this will affect your setup. ``` Name the vpn server: -[algo.local]: +[algo.local]: ``` -After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. +After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. ``` What region should the server be located in? @@ -83,5 +83,5 @@ If you are using Ansible to deploy to DigitalOcean, you will need to pass the AP For example, ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2 - + Where "my_secret_token" is your API Token. diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 5516611b..13113a38 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,8 +1,8 @@ # Local deployment -It is possible to download the Algo scripts to your own Ubuntu 16.04 server and run the scripts locally. +It is possible to download the Algo scripts to your own Ubuntu 18.04 server and run the scripts locally. -In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 16.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. +In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 18.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. tl;dr: diff --git a/docs/deploy-to-unsupported-cloud.md b/docs/deploy-to-unsupported-cloud.md index 3e1e5dab..7fd176f7 100644 --- a/docs/deploy-to-unsupported-cloud.md +++ b/docs/deploy-to-unsupported-cloud.md @@ -2,7 +2,7 @@ Algo officially supports DigitalOcean, Amazon Web Services, Microsoft Azure, and Google Cloud Engine. If you want to deploy Algo on another virtual hosting provider, that provider must support: -1. the base operating system image that Algo uses (Ubuntu 16.04), and +1. the base operating system image that Algo uses (Ubuntu 18.04), and 2. a minimum of certain kernel modules required for the strongSwan IPsec server. Please see the [Required Kernel Modules](https://wiki.strongswan.org/projects/strongswan/wiki/KernelModules) documentation from strongSwan for a list of the specific required modules and a script to check for them. As a first step, we recommend running their shell script to determine initial compatibility with your new hosting provider. diff --git a/docs/index.md b/docs/index.md index e5c7050c..47705b7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,8 +14,7 @@ - Configure [DigitalOcean](cloud-do.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - - Deploy to your own [Ubuntu 16.04](deploy-to-ubuntu.md) server + - Deploy to your own [Ubuntu 18.04](deploy-to-ubuntu.md) server - Deploy to an [unsupported cloud provider](deploy-to-unsupported-cloud.md) * [FAQ](faq.md) * [Troubleshooting](troubleshooting.md) - diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 09f3fbf8..6dbc79e8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -198,7 +198,7 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. -E.g., On Linux (client -- Ubuntu 16.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: +E.g., On Linux (client -- Ubuntu 18.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: ``` $ ping -M do -s 1500 www.google.com PING www.google.com (74.125.22.147) 1500(1528) bytes of data. From 16e78087d175c79b1a7c27b0314afdaefa7a9ed7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 30 May 2018 17:17:08 +0300 Subject: [PATCH 633/769] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d3028a5..da715362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 24 May 2018 +### Changed +- Switched to Ubuntu 18.04 + +### Removed +- Lightsail support until they have Ubuntu 18.04 + +### Fixed +- Scaleway API paginagion + ## 30 Apr 2018 ### Added - WireGuard support From d7bce687388638e871a20bc8e5a83a8c64c69b34 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Thu, 31 May 2018 19:32:41 +0300 Subject: [PATCH 634/769] TravisCI fixes --- .travis.yml | 2 -- tests/local-deploy.sh | 3 ++- tests/update-users.sh | 12 +++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2a2fa1d9..9d91089e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -70,8 +70,6 @@ script: # - ansible-lint deploy.yml users.yml deploy_client.yml - ansible-playbook deploy.yml --syntax-check - ./tests/local-deploy.sh - -after_script: - ./tests/update-users.sh notifications: diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index 246132f8..b82ea149 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -3,10 +3,11 @@ set -ex DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false install_headers=false" +touch /tmp/ca_password if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor,wireguard" + docker run -it -v /tmp/ca_password:/tmp/ca_password -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor,wireguard" else ansible-playbook deploy.yml -t cloud,local,vpn,dns,dns_over_https,ssh_tunneling,tests -e "${DEPLOY_ARGS}" --skip-tags apparmor fi diff --git a/tests/update-users.sh b/tests/update-users.sh index 8122a156..bea5a8cb 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -3,20 +3,18 @@ set -ex CAPW=`cat /tmp/ca_password` -USER_ARGS="server_ip=$LXC_IP server_user=ubuntu ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW" +USER_ARGS="server_ip=$LXC_IP server_user=ubuntu ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW apparmor_enabled=false install_headers=false" sed -i 's/- jack$/- jack_test/' config.cfg if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\"" + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\" -t update-users --skip-tags common" else - ansible-playbook users.yml -e "${USER_ARGS}" + ansible-playbook users.yml -e "${USER_ARGS}" -t update-users --skip-tags common fi -cd configs/$LXC_IP/pki/ - -if openssl crl -inform pem -noout -text -in crl/jack.crt | grep CRL +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.crt | grep CRL then echo "The CRL check passed" else @@ -24,7 +22,7 @@ if openssl crl -inform pem -noout -text -in crl/jack.crt | grep CRL exit 1 fi -if openssl x509 -inform pem -noout -text -in certs/jack_test.crt | grep CN=jack_test +if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/jack_test.crt | grep CN=jack_test then echo "The new user exists" else From ffb5a1f737d1a77db43697bf10067c284eee4b3a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 1 Jun 2018 17:06:03 +0300 Subject: [PATCH 635/769] WireGuard: disable SaveConfig, update-users fix (#985) - Disables SaveConfig. SaveConfig totally breaks the idea of configuration management and it breaks update-users - WireGuard update-users fix. Mentioned in https://github.com/trailofbits/algo/issues/980#issuecomment-393720561 --- roles/wireguard/templates/server.conf.j2 | 2 +- users.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index 3f9f45dd..17b388fc 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -4,7 +4,7 @@ Address = {{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['pref ListenPort = {{ wireguard_port }} PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} -SaveConfig = true +SaveConfig = false Table = off {% for u in users %} diff --git a/users.yml b/users.yml index 46a2d79c..f60cbb3b 100644 --- a/users.yml +++ b/users.yml @@ -55,6 +55,7 @@ roles: - { role: ssh_tunneling, tags: always, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } + - { role: wireguard, tags: [ 'vpn', 'wireguard' ], when: wireguard_enabled } - { role: vpn } post_tasks: From 030cb9a83036a8a2266acdb1f5182001a61f2396 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Fri, 1 Jun 2018 17:41:30 +0300 Subject: [PATCH 636/769] Test fixes --- tests/local-deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index b82ea149..b586aaac 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -7,7 +7,7 @@ touch /tmp/ca_password if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v /tmp/ca_password:/tmp/ca_password -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor,wireguard" + docker run -it -v /tmp/ca_password:/tmp/ca_password -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" else ansible-playbook deploy.yml -t cloud,local,vpn,dns,dns_over_https,ssh_tunneling,tests -e "${DEPLOY_ARGS}" --skip-tags apparmor fi From 6faac307afe98465a3d8bf9f7ddd6566dd8a6506 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 4 Jun 2018 11:09:01 -0400 Subject: [PATCH 637/769] Update troubleshooting.md (#992) Many times people are reaching VPC limits not because they're running other VPCs on AWS, but because they've already deployed several times (AWS allows five VPCs per region). This lets people know they can simply delete their old VPCs instead of contacting AWS support. --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6dbc79e8..4fbcdc65 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -160,7 +160,7 @@ fatal: [localhost]: FAILED! => {"changed": true, "events": ["StackEvent AWS::Clo Algo builds a [Cloudformation](https://aws.amazon.com/cloudformation/) template to deploy to AWS. You can find the entire contents of the Cloudformation template in `configs/algo.yml`. In order to troubleshoot this issue, login to the AWS console, go to the Cloudformation service, find the failed deployment, click the events tab, and find the corresponding "CREATE_FAILED" events. Note that all AWS resources created by Algo are tagged with `Environment => Algo` for easy identification. -In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. +In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must either [delete the VPCs from previous deployments](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/working-with-vpcs.html#VPC_Deleting), or [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. ## Connection Problems From 2f142f6dccdff933a552a3939e4904fa67851f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Beganovi=C4=87?= Date: Mon, 25 Jun 2018 12:40:51 +0200 Subject: [PATCH 638/769] Remove duplicate dict key (enable_ipv6) (#999) Warning in yaml file: ` [WARNING]: While constructing a mapping from /root/algo/roles/cloud-scaleway/tasks/main.yml, line 73, column 11, found a duplicate dict key (enable_ipv6). Using last defined value only.` --- roles/cloud-scaleway/tasks/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 31cc3f99..1bc939b8 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -79,7 +79,6 @@ tags: - Environment:Algo - AUTHORIZED_KEY={{ lookup('file', SSH_keys.public)|regex_replace(' ', '_') }} - enable_ipv6: true status_code: 201 body_format: json register: algo_instance From 2931227db4286233d660dade4876ee6cf93be1ff Mon Sep 17 00:00:00 2001 From: Mikael Forsgren Date: Tue, 26 Jun 2018 12:01:45 +0200 Subject: [PATCH 639/769] New Google Cloud Region (#1013) Added the new Google Cloud Region Finland (europe-north1) with 3 zones --- algo | 114 +++++++++++++++++++----------------- docs/deploy-from-ansible.md | 3 + 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/algo b/algo index 73e39657..3c17a7d8 100755 --- a/algo +++ b/algo @@ -444,33 +444,36 @@ Name the vpn server: 17. South America East (São Paulo A) 18. South America East (São Paulo B) 19. South America East (São Paulo C) - 20. Western Europe (Belgium B) - 21. Western Europe (Belgium C) - 22. Western Europe (Belgium D) - 23. Western Europe (London A) - 24. Western Europe (London B) - 25. Western Europe (London C) - 26. Western Europe (Frankfurt A) - 27. Western Europe (Frankfurt B) - 28. Western Europe (Frankfurt C) - 29. Western Europe (Netherlands A) - 30. Western Europe (Netherlands B) - 31. Western Europe (Netherlands C) - 32. South Asia (Mumbai A) - 33. South Asia (Mumbai B) - 34. South Asia (Mumbai C) - 35. Southeast Asia (Singapore A) - 36. Southeast Asia (Singapore B) - 37. Southeast Asia (Singapore C) - 38. East Asia (Taiwan A) - 39. East Asia (Taiwan B) - 40. East Asia (Taiwan C) - 41. Northeast Asia (Tokyo A) - 42. Northeast Asia (Tokyo B) - 43. Northeast Asia (Tokyo C) - 44. Australia (Sydney A) - 45. Australia (Sydney B) - 46. Australia (Sydney C) + 20. Northern Europe (Hamina A) + 21. Northern Europe (Hamina B) + 22. Northern Europe (Hamina C) + 23. Western Europe (Belgium B) + 24. Western Europe (Belgium C) + 25. Western Europe (Belgium D) + 26. Western Europe (London A) + 27. Western Europe (London B) + 28. Western Europe (London C) + 29. Western Europe (Frankfurt A) + 30. Western Europe (Frankfurt B) + 31. Western Europe (Frankfurt C) + 32. Western Europe (Netherlands A) + 33. Western Europe (Netherlands B) + 34. Western Europe (Netherlands C) + 35. South Asia (Mumbai A) + 36. South Asia (Mumbai B) + 37. South Asia (Mumbai C) + 38. Southeast Asia (Singapore A) + 39. Southeast Asia (Singapore B) + 40. Southeast Asia (Singapore C) + 41. East Asia (Taiwan A) + 42. East Asia (Taiwan B) + 43. East Asia (Taiwan C) + 44. Northeast Asia (Tokyo A) + 45. Northeast Asia (Tokyo B) + 46. Northeast Asia (Tokyo C) + 47. Australia (Sydney A) + 48. Australia (Sydney B) + 49. Australia (Sydney C) Please choose the number of your zone. Press enter for default (#20) zone. [20]: " -r region @@ -496,33 +499,36 @@ Please choose the number of your zone. Press enter for default (#20) zone. 17) zone="southamerica-east1-a" ;; 18) zone="southamerica-east1-b" ;; 19) zone="southamerica-east1-c" ;; - 20) zone="europe-west1-b" ;; - 21) zone="europe-west1-c" ;; - 22) zone="europe-west1-d" ;; - 23) zone="europe-west2-a" ;; - 24) zone="europe-west2-b" ;; - 25) zone="europe-west2-c" ;; - 26) zone="europe-west3-a" ;; - 27) zone="europe-west3-b" ;; - 28) zone="europe-west3-c" ;; - 29) zone="europe-west4-a" ;; - 30) zone="europe-west4-b" ;; - 31) zone="europe-west4-c" ;; - 32) zone="asia-south1-a" ;; - 33) zone="asia-south1-b" ;; - 34) zone="asia-south1-c" ;; - 35) zone="asia-southeast1-a" ;; - 36) zone="asia-southeast1-b" ;; - 37) zone="asia-southeast1-c" ;; - 38) zone="asia-east1-a" ;; - 39) zone="asia-east1-b" ;; - 40) zone="asia-east1-c" ;; - 41) zone="asia-northeast1-a" ;; - 42) zone="asia-northeast1-b" ;; - 43) zone="asia-northeast1-c" ;; - 44) zone="australia-southeast1-a" ;; - 45) zone="australia-southeast1-b" ;; - 46) zone="australia-southeast1-c" ;; + 20) zone="europe-north1-a" ;; + 21) zone="europe-north1-b" ;; + 22) zone="europe-north1-c" ;; + 23) zone="europe-west1-b" ;; + 24) zone="europe-west1-c" ;; + 25) zone="europe-west1-d" ;; + 26) zone="europe-west2-a" ;; + 27) zone="europe-west2-b" ;; + 28) zone="europe-west2-c" ;; + 29) zone="europe-west3-a" ;; + 30) zone="europe-west3-b" ;; + 31) zone="europe-west3-c" ;; + 32) zone="europe-west4-a" ;; + 33) zone="europe-west4-b" ;; + 34) zone="europe-west4-c" ;; + 35) zone="asia-south1-a" ;; + 36) zone="asia-south1-b" ;; + 37) zone="asia-south1-c" ;; + 38) zone="asia-southeast1-a" ;; + 39) zone="asia-southeast1-b" ;; + 40) zone="asia-southeast1-c" ;; + 41) zone="asia-east1-a" ;; + 42) zone="asia-east1-b" ;; + 43) zone="asia-east1-c" ;; + 44) zone="asia-northeast1-a" ;; + 45) zone="asia-northeast1-b" ;; + 46) zone="asia-northeast1-c" ;; + 47) zone="australia-southeast1-a" ;; + 48) zone="australia-southeast1-b" ;; + 49) zone="australia-southeast1-c" ;; esac ROLES="gce vpn cloud" diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index e6fb2b05..f7bcf6da 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -198,6 +198,9 @@ Possible options for `zone`: - us-east1-b - us-east1-c - us-east1-d +- europe-north1-a +- europe-north1-b +- europe-north1-c - europe-west1-b - europe-west1-c - europe-west1-d From b061df66310f656ac555c03764bf2f64817d01b5 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 26 Jun 2018 13:11:09 +0300 Subject: [PATCH 640/769] Move DNSCrypt proxy fallback_resolver to systemd resolved (#1011) --- roles/common/tasks/ubuntu.yml | 7 +++++-- roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index b0f347d7..f2799ab0 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -57,12 +57,15 @@ tags: - always -- name: systemd-networkd enabled and started +- name: systemd services enabled and started systemd: - name: systemd-networkd + name: "{{ item }}" state: started enabled: true daemon_reload: true + with_items: + - systemd-networkd + - systemd-resolved tags: - always diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 72eb898d..22e9cfc5 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -151,7 +151,7 @@ tls_cipher_suite = [49195] ## People in China may need to use 114.114.114.114:53 here. ## Other popular options include 8.8.8.8 and 1.1.1.1. -fallback_resolver = '1.1.1.1:53' +fallback_resolver = '127.0.0.53:53' ## Never try to use the system DNS settings; unconditionally use the From 4ca8c03e3c952981ada128525e6ee5039a520af6 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 27 Jun 2018 18:22:45 +0300 Subject: [PATCH 641/769] New default cipher suite (#991) * New ciphers enabled * Update CHANGELOG.md * Switch ecparam to secp384r1 * Change CertificateType to ECDSA384 --- CHANGELOG.md | 4 ++++ docs/client-linux.md | 4 ++-- docs/client-windows.md | 10 +++++----- roles/vpn/defaults/main.yml | 8 ++++---- roles/vpn/tasks/openssl.yml | 8 ++++---- roles/vpn/templates/client_windows.ps1.j2 | 10 +++++----- roles/vpn/templates/mobileconfig.j2 | 10 +++++----- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da715362..897352b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 04 Jun 2018 +### Changed +- Switched to [new cipher suite](https://github.com/trailofbits/algo/issues/981) + ## 24 May 2018 ### Changed - Switched to Ubuntu 18.04 diff --git a/docs/client-linux.md b/docs/client-linux.md index a24eda1d..94a6445f 100644 --- a/docs/client-linux.md +++ b/docs/client-linux.md @@ -73,6 +73,6 @@ In this example we'll assume the IP of our Algo VPN server is `1.2.3.4` and the * For the later 2 options, hover to option in the settings to see a description * Cipher proposal: * Check *Enable custom proposals* - * IKE: `aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256` - * ESP: `aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256` + * IKE: `aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384` + * ESP: `aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384` * Apply and turn the connection on, you should now be connected diff --git a/docs/client-windows.md b/docs/client-windows.md index d7d89151..6e071cf1 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -48,12 +48,12 @@ Add-VpnConnection @addVpnParams $setVpnParams = @{ ConnectionName = $VpnName - AuthenticationTransformConstants = "GCMAES128" - CipherTransformConstants = "GCMAES128" - EncryptionMethod = "AES128" + AuthenticationTransformConstants = "GCMAES256" + CipherTransformConstants = "GCMAES256" + EncryptionMethod = "AES256" IntegrityCheckMethod = "SHA384" - DHGroup = "ECP256" - PfsGroup = "ECP256" + DHGroup = "ECP384" + PfsGroup = "ECP384" Force = $true } Set-VpnConnectionIPsecConfiguration @setVpnParams diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 2efc124d..f969fb29 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -25,8 +25,8 @@ strongswan_enabled_plugins: ciphers: defaults: - ike: aes128gcm16-prfsha512-ecp256! - esp: aes128gcm16-ecp256! + ike: aes256gcm16-prfsha512-ecp384! + esp: aes256gcm16-ecp384! compat: - ike: aes128gcm16-prfsha512-ecp256,aes128-sha2_512-prfsha512-ecp256,aes128-sha2_384-prfsha384-ecp256! - esp: aes128gcm16-ecp256,aes128-sha2_512-prfsha512-ecp256! + ike: aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384! + esp: aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384! diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index 053470fb..af19ae2b 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -42,9 +42,9 @@ - name: Build the CA pair shell: > - {{ openssl_bin }} ecparam -name prime256v1 -out ecparams/prime256v1.pem && + {{ openssl_bin }} ecparam -name secp384r1 -out ecparams/secp384r1.pem && {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/prime256v1.pem + -newkey ec:ecparams/secp384r1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 @@ -71,7 +71,7 @@ - name: Build the server pair shell: > {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/prime256v1.pem + -newkey ec:ecparams/secp384r1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes @@ -93,7 +93,7 @@ - name: Build the client's pair shell: > {{ openssl_bin }} req -utf8 -new - -newkey ec:ecparams/prime256v1.pem + -newkey ec:ecparams/secp384r1.pem -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 93269c7f..4ffce674 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -169,12 +169,12 @@ function Add-AlgoVPN { $setVpnParams = @{ ConnectionName = $VpnName - AuthenticationTransformConstants = "GCMAES128" - CipherTransformConstants = "GCMAES128" - EncryptionMethod = "AES128" + AuthenticationTransformConstants = "GCMAES256" + CipherTransformConstants = "GCMAES256" + EncryptionMethod = "AES256" IntegrityCheckMethod = "SHA384" - DHGroup = "ECP256" - PfsGroup = "ECP256" + DHGroup = "ECP384" + PfsGroup = "ECP384" Force = $true } Set-VpnConnectionIPsecConfiguration @setVpnParams diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index b8013df2..9a342b4b 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -60,9 +60,9 @@ ChildSecurityAssociationParameters DiffieHellmanGroup - 19 + 20 EncryptionAlgorithm - AES-128-GCM + AES-256-GCM IntegrityAlgorithm SHA2-512 LifeTimeInMinutes @@ -81,9 +81,9 @@ IKESecurityAssociationParameters DiffieHellmanGroup - 19 + 20 EncryptionAlgorithm - AES-128-GCM + AES-256-GCM IntegrityAlgorithm SHA2-512 LifeTimeInMinutes @@ -94,7 +94,7 @@ PayloadCertificateUUID {{ pkcs12_PayloadCertificateUUID }} CertificateType - ECDSA256 + ECDSA384 ServerCertificateIssuerCommonName {{ IP_subject_alt_name }} RemoteAddress From d1c58f0d282fcf7a4e15ac8aa7def680b944460a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 2 Jul 2018 16:33:31 +0300 Subject: [PATCH 642/769] apt_repository fix (#1017) --- roles/dns_encryption/tasks/ubuntu.yml | 6 +++++- roles/wireguard/tasks/main.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 0f1cffcf..5485f682 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -4,7 +4,11 @@ state: present codename: artful repo: ppa:shevchuk/dnscrypt-proxy - + register: result + until: result|succeeded + retries: 10 + delay: 3 + - name: Install dnscrypt-proxy apt: name: dnscrypt-proxy diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 017e2ac3..4b70a3a2 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -3,6 +3,10 @@ apt_repository: repo: ppa:wireguard/wireguard state: present + register: result + until: result|succeeded + retries: 10 + delay: 3 - name: WireGuard installed apt: From 07a6bbe652d42529cce367b48ae362f229b35a52 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 3 Jul 2018 09:06:45 +0300 Subject: [PATCH 643/769] Move max_mss to config.cfg (#1015) * Move max_mss to config.cfg * Add docs about max_mss * Update troubleshooting.md --- config.cfg | 10 ++++++++++ docs/troubleshooting.md | 6 +++++- roles/vpn/templates/rules.v4.j2 | 8 -------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/config.cfg b/config.cfg index 731c71de..a8fa915a 100644 --- a/config.cfg +++ b/config.cfg @@ -18,6 +18,16 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true wireguard_port: 51820 +# MSS is the TCP Max Segment Size +# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation +# This appears to be necessary on (at least) Google Cloud, +# however, some routers also require a change to this parameter +# See also: +# - https://github.com/trailofbits/algo/issues/216 +# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu +# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan +#max_mss: 1316 + server_name: "{{ ansible_ssh_host }}" IP_subject_alt_name: "{{ ansible_ssh_host }}" diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4fbcdc65..c16ed9fb 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -196,7 +196,9 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network ### Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. If you experience this issue, we recommend [filing an issue](https://github.com/trailofbits/algo/issues/new) for assistance. Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +This issue appears intermittently due to issues with MTU size. Different networks may require the MTU within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. + +Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. E.g., On Linux (client -- Ubuntu 18.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: ``` @@ -209,6 +211,8 @@ Then, set the MTU size on your network adapter (wlan0 or eth0): $ sudo ifconfig wlan0 mtu 1438 ``` +You can also set the `max_mss` variable to a new value in config.cfg, and then redeploy your server rather than reconfigure the current one in-place. + ### "Error 809" or IKE_AUTH requests that never make it to the server On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index fe2878d6..dbcc368f 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -11,14 +11,6 @@ :POSTROUTING ACCEPT [0:0] {% if max_mss is defined %} -# MSS is the TCP Max Segment Size -# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation -# This appears to be necessary on (at least) Google Cloud, -# however, some routers also require a change to this parameter -# See also: -# - https://github.com/trailofbits/algo/issues/216 -# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu -# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan -A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} {% endif %} From facd55c6355a50a4e45e9f223da30321c6bf78c4 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 3 Jul 2018 10:02:54 -0400 Subject: [PATCH 644/769] Update deploy-to-ubuntu.md (#1019) * Update deploy-to-ubuntu.md rewrite of #813 * Update deploy-to-ubuntu.md --- docs/deploy-to-ubuntu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 13113a38..36956a30 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -17,4 +17,4 @@ python -m virtualenv env && source env/bin/activate && python -m pip install -U ./algo ``` -**Warning**: If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). +**Warning**: Algo is intended to be run on a standalone server. If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). Other changes are also made, which can break other services running on your server (web, mail, etc.). From e6281bc7dfca226cc6d63f9a5ab5ffd490142ff2 Mon Sep 17 00:00:00 2001 From: adamluk Date: Thu, 12 Jul 2018 15:03:36 +0100 Subject: [PATCH 645/769] Update dnscrypt-proxy.toml.j2 (#1022) --- .../dns_encryption/templates/dnscrypt-proxy.toml.j2 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 22e9cfc5..c5bd6ccc 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -41,6 +41,18 @@ listen_addresses = ['{{ local_service_ip }}:{{ listen_port }}'] max_clients = 250 +## Switch to a non-privileged system user after listening sockets have been created. +## Two processes will be running. +## The first one will keep root privileges, but is only a supervisor, that does nothing +## except create the sockets, manage the service, and restart it if it crashes. +## The second process is the service itself, and that one will always run as a different +## user. +## Note (1): this feature is currently unsupported on Windows. +## Note (2): this feature is not compatible with systemd socket activation. + +user_name = 'nobody' + + ## Require servers (from static + remote sources) to satisfy specific properties # Use servers reachable over IPv4 From 952e759af4d7c921aabe37eda7049a22dfdd913f Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 20 Jul 2018 09:48:59 +0300 Subject: [PATCH 646/769] Revert "Update dnscrypt-proxy.toml.j2 (#1022)" (#1030) This reverts commit e6281bc7dfca226cc6d63f9a5ab5ffd490142ff2. --- .../dns_encryption/templates/dnscrypt-proxy.toml.j2 | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index c5bd6ccc..22e9cfc5 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -41,18 +41,6 @@ listen_addresses = ['{{ local_service_ip }}:{{ listen_port }}'] max_clients = 250 -## Switch to a non-privileged system user after listening sockets have been created. -## Two processes will be running. -## The first one will keep root privileges, but is only a supervisor, that does nothing -## except create the sockets, manage the service, and restart it if it crashes. -## The second process is the service itself, and that one will always run as a different -## user. -## Note (1): this feature is currently unsupported on Windows. -## Note (2): this feature is not compatible with systemd socket activation. - -user_name = 'nobody' - - ## Require servers (from static + remote sources) to satisfy specific properties # Use servers reachable over IPv4 From ca59eeb5c31b11ed086d8aa1d56676c512e3ec3a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 20 Jul 2018 17:31:27 +0300 Subject: [PATCH 647/769] Explicitly allow traffic between clients if enabled (#1028) --- roles/vpn/templates/rules.v4.j2 | 5 +++-- roles/vpn/templates/rules.v6.j2 | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index dbcc368f..820589f3 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -69,10 +69,11 @@ COMMIT # Accept DNS traffic to the local DNS resolver -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT -{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} # Drop traffic between VPN clients --A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j DROP +{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +{% set BetweenClientsPolicy = "DROP" %} {% endif %} +-A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} # Forward any packet that's part of an established connection -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index df0603a8..4f00c309 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -84,9 +84,12 @@ COMMIT # Accept DNS traffic to the local DNS resolver -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT +# Drop traffic between VPN clients {% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} --A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j DROP +{% set BetweenClientsPolicy = "DROP" %} {% endif %} +-A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} + -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP From c65961a1f3db68d919ac17cc99bed7ce97946639 Mon Sep 17 00:00:00 2001 From: Mike Myers Date: Sun, 22 Jul 2018 14:58:09 -0700 Subject: [PATCH 648/769] Amazon ec2 documentation (#1035) * Add link to documentation on Amazon EC2 setup * Add images to document the AWS EC2 account setup * Create AWS EC2 setup instructions * remove line breaks * remove line breaks * Add images documenting AWS EC2 policy creation * Update image showing advised minimum AWS policy * Add instructions for minimum AWS permission policy * Delete aws-ec2-attach-policy.png * Updated image to reflect new AWS policy guidance * Delete aws-ec2-new-user-confirm.png * Updated image to reflect new AWS policy guidance --- README.md | 1 + docs/cloud-amazon-ec2.md | 114 ++++++++++++++++++++++ docs/images/aws-ec2-attach-policy.png | Bin 0 -> 98275 bytes docs/images/aws-ec2-new-policy-review.png | Bin 0 -> 104314 bytes docs/images/aws-ec2-new-policy.png | Bin 0 -> 82947 bytes docs/images/aws-ec2-new-user-confirm.png | Bin 0 -> 78745 bytes docs/images/aws-ec2-new-user-csv.png | Bin 0 -> 77993 bytes docs/images/aws-ec2-new-user-name.png | Bin 0 -> 104457 bytes docs/images/aws-ec2-new-user.png | Bin 0 -> 112857 bytes 9 files changed, 115 insertions(+) create mode 100644 docs/cloud-amazon-ec2.md create mode 100644 docs/images/aws-ec2-attach-policy.png create mode 100644 docs/images/aws-ec2-new-policy-review.png create mode 100644 docs/images/aws-ec2-new-policy.png create mode 100644 docs/images/aws-ec2-new-user-confirm.png create mode 100644 docs/images/aws-ec2-new-user-csv.png create mode 100644 docs/images/aws-ec2-new-user-name.png create mode 100644 docs/images/aws-ec2-new-user.png diff --git a/README.md b/README.md index 6f0b42c1..8f5ca043 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ After this process completes, the Algo VPN server will contains only the users l - Setup [Android](docs/client-android.md) clients - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible * Cloud setup + - Configure [Amazon EC2](docs/cloud-amazon-ec2.md) - Configure [Azure](docs/cloud-azure.md) - Configure [DigitalOcean](docs/cloud-do.md) * Advanced Deployment diff --git a/docs/cloud-amazon-ec2.md b/docs/cloud-amazon-ec2.md new file mode 100644 index 00000000..63831d55 --- /dev/null +++ b/docs/cloud-amazon-ec2.md @@ -0,0 +1,114 @@ +# Amazon EC2 cloud setup + +## AWS account creation + +Creating an Amazon AWS account requires giving Amazon a phone number that can receive a call and has a number pad to enter a PIN challenge displayed in the browser. This phone system prompt occasionally fails to correctly validate input, but try again (request a new PIN in the browser) until you succeed. + +### Select an EC2 plan + +The cheapest EC2 plan you can choose is the "Free Plan" a.k.a. the "AWS Free Tier." It is only available to new AWS customers, it has limits on usage, and is converts to standard pricing after 12 months (the "introductory period"). After you exceed the usage limits, after the 12 month period, or if you are an existing AWS customer, then you will pay standard pay-as-you-go service prices. + +*Note*: Your Algo instance will not stop working when you hit the bandwidth limit, you will just start accumulating service charges on your AWS account. + +As of the time of this writing (July 2018), the Free Tier limits include "750 hours of Amazon EC2 Linux t2.micro instance usage" per month, 15 GB of bandwidth (outbound) per month, and 30 GB of cloud storage. Algo will not even use 1% of the storage limit, but you may have to monitor your bandwidth usage or keep an eye out for the email from Amazon when you are about to exceed the Free Tier limits. + +### Create an AWS permissions policy + +In the AWS console, find the policies menu: click Services > IAM > Policies. Click Create Policy. + +Here, you have the policy editor. Switch to the JSON tab and copy-paste over the existing empty policy with [the minimum required AWS policy needed for Algo deployment](https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md#minimum-required-iam-permissions-for-deployment). + +![Creating a new permissions policy in the AWS console.](/docs/images/aws-ec2-new-policy.png) + +### Set up an AWS user + +In the AWS console, find the users (“Identiy and Access Management”, a.k.a. IAM users) menu: click Services > IAM. + +Activate multi-factor authentication (MFA) on your root account. The simplest choice is the mobile app "Google Authenticator." A hardware U2F token is ideal (less prone to a phishing attack), but a TOTP authenticator like this is good enough. + +![The new user screen in the AWS console.](/docs/images/aws-ec2-new-user.png) + +Now "Create individual IAM users" and click Add User. Create a user name. I chose “algovpn”. Then click the box next to Programmatic Access. Then click Next. + +![The IAM user naming screen in the AWS console.](/docs/images/aws-ec2-new-user-name.png) + +Next, click “Attach existing policies directly.” Type “Algo” in the search box to filter the policies. Find “AlgoVPN_Provisioning” (the policy you created) and click the checkbox next to that. Click Next when you’re done. + +![Attaching a policy to an IAM user in the AWS console.](/docs/images/aws-ec2-attach-policy.png) + +The user creation confirmation screen should look like this if you've done everything correctly. + +![New user creation confirmation screen in the AWS console.](/docs/images/aws-ec2-new-user-confirm.png) + +On the final screen, click the Download CSV button. This file includes the AWS access keys you’ll need during the Algo set-up process. Click Close, and you’re all set. + +![Downloading the credentials for an AWS IAM user.](/docs/images/aws-ec2-new-user-csv.png) + +## Using EC2 during Algo setup + +After you have downloaded Algo and installed its dependencies, the next step is running Algo to provision the VPN server on your AWS account. + +First you will be asked which server type to setup. You would want to enter "2" to use Amazon EC2. + +``` +$ ./algo + + What provider would you like to use? + 1. DigitalOcean + 2. Amazon EC2 + 3. Microsoft Azure + 4. Google Compute Engine + 5. Scaleway + 6. OpenStack (DreamCompute optimised) + 7. Install to existing Ubuntu 16.04 server (Advanced) + +Enter the number of your desired provider +: 2 +``` + +Next you will be asked for the AWS Access Key (Access Key ID) and AWS Secret Key (Secret Access Key) that you received in the CSV file when you setup the account (don't worry if you don't see your text entered in the console; the key input is hidden here by Algo). + +``` +Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). +[pasted values will not be displayed] +[AKIA...]: + +Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) +[pasted values will not be displayed] +[ABCD...]: +``` + +You will be prompted for the server name to enter. Feel free to leave this as the default ("algo") if you are not certain how this will affect your setup. Here we chose to call it "algovpn". + +``` +Name the vpn server: +[algo]: algovpn +``` + +After entering the server name, the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. + +``` + What region should the server be located in? + 1. us-east-1 US East (N. Virginia) + 2. us-east-2 US East (Ohio) + 3. us-west-1 US West (N. California) + 4. us-west-2 US West (Oregon) + 5. ca-central-1 Canada (Central) + 6. eu-central-1 EU (Frankfurt) + 7. eu-west-1 EU (Ireland) + 8. eu-west-2 EU (London) + 9. eu-west-3 EU (Paris) + 10. ap-northeast-1 Asia Pacific (Tokyo) + 11. ap-northeast-2 Asia Pacific (Seoul) + 12. ap-northeast-3 Asia Pacific (Osaka-Local) + 13. ap-southeast-1 Asia Pacific (Singapore) + 14. ap-southeast-2 Asia Pacific (Sydney) + 15. ap-south-1 Asia Pacific (Mumbai) + 16. sa-east-1 South America (São Paulo) + +Enter the number of your desired region: +[1]: 10 +``` + +You will then be asked the remainder of the standard Algo setup questions. diff --git a/docs/images/aws-ec2-attach-policy.png b/docs/images/aws-ec2-attach-policy.png new file mode 100644 index 0000000000000000000000000000000000000000..00108240f74ab5c35e18b72d606890c44edac0ec GIT binary patch literal 98275 zcmafab9`pOl4vj!PHfw@ZA@%qCYac^ZQHhO+qSJQ$rr!5cX#jJ{k^xp*MD_ab*cMQ zojz5k!xZEs;9#&}KtMp?q$EX^KtR3~fPjE5KtcS4yzOqdfPi3fn2U%gNQsCLD%jhY zm|Ok^0nrFba)VSs{n_9=Gi{RG>U)^!5KG8qI;Qy@JsDS*nOYhV6$upyNdXa26crT> zN$5K?1gh9~aAHjbQN*~ydjz4KXYU=ZFTR$UvozC+f424xXOP`(>99~JAtq2nY;>^( z4mLzvCkKpPIB=+1Fyvkkr*QdSf!$SAXoBN8&tJ3x?nu^!ow2$Jk2_aiJN>ecQ23U#H{GXw?!Lo%>WRcjI!L`YR8Y2|G!ry%@5clGs;ou|~IiUNH zx4+^Kz65@`ey9wY4&wHOUMWWVDIQ1TcqyuAwSE+Ps$|*})(sHI=2gWs4JRPsT^9CGJy`nIybz>C zL4!uqsU&f3@N$zKjdu&5H5{hp$1(dOy=? z5alcAaPiX;4Q_6VWA@n^Mj1--AEF@d5Af`9g6j9M#w0&F#HphdFb2|+!DVi`i;H)agFmM{hh z0s1~=IP#jiE!)@)+a#EgB%G)k{)mSz0QZ{Oo&v4_#Xw}Be!&}IdoZC>)m4ufSfr1c zhH)j*@(&CDBvFSvr`dEfn!~aX6|zQd_YH!s&%KdI<80$#rW(e8RE})FvBs}i9S}>% zfY502jE0BL=gV26P!+UBP%WIem{IYDf;L}TcN*q+ek}|v?eBS?+AF6$ET1FE8TC*w z-%t%P^agHjG3VZhDrP-#6}a9vI(KK1xS7?ghB^L~hwyQLOz4 z`hVX3Bm~(B!r7&7b%XnQjGA-W0kWijwDb3cX7+wz$JBEjr!Ntsm7|fPd0RL8QSP*s zHW@JehzTwE{t77yeM-{NRO9QG8j~668}CnCA5nevfulW<`SF?!U+3~A@}LeT_!~V8 z@6N}}uLPtuY7o!*S%8&k@B~W>#5MH-lA_XtV~_>MOOzD=6q6|>7U>fO{%}x(CWVg+ z9~DR|rYJm?qAwaR(kv35v;4t_V8ok*hwZPg<+g;{=)JBbkPSfD`@xAoA7rsB{;Np8 zH{cevGjKS6%nY=VcE85NjG__2or4=GD~w)HUvQ;hW0ql78W9=2`OVZ^Erz~E>4ki)CG`}oNaDIiX4XsYCiL5rPU95VnrL^2y&+Y7Y9Ni|T)A4De zbGC8;I6b+TIsb6!+0i<{*kU_CIN+ajj=f~VPLU1$9zmOMO0LMV2+v5;PS^jBw8GGk`_j_-9cX<(j zu7y5@4TL&`ZG@hLx`#@K7KhD7@uY4gCZ#_lO{7(!0qCVtR?=TmWYTO>^OE#4)a(AL zk!%{(R`f4zH}gHEnvqkkU75nE;(Ym2{RsHg-NSJgPPUKUO!PIYu_I zm6(x?5$BR3K)*xvr7a{zB$}tQmqi?rA7oT-RItxMMny(YBwUm-4j3mNe@ntiG;NW# zdE9b$zOX>6A9qf2Oj1kIFaJ}{x`e$X#fHSj&_vwC@1*9W{V4kgxxu*cqxouQxzS2W zM3qFkK<=QTxYDHdSZoTK&W=vM%DxK8ipVPbiR(%T4-}6f3pqjO###6k$7A)?o!Z!$?U^Ru+F zu(DjZ)wvj2G20GXf3%NxTswGO4j-YPF7HM=xH#9?=Ij`D9vnv3Wl{4iI6qw@@1V6@ z5U{s{wHPTOff{{? z_mXy&@N!zZKlffxq3NwD-=NwsgFk=|%d_mE^9&5aJOwUp#?1G&%GZICHx< z++Ob7R~}TJD}i{Zy2p`=$mDl^H5C%eNBj zxPQ$&YkO6yo9|?{)RbLwZF6utaeI7FMbkl(p!QY`R)yEA?U_|8Qd4z`J27%*E9Y)^ zzn`C$nW4wgwJwdT;!>Z`^EiZQMfN&cP_3!xa!PipT3M`F(!uD`a}4k#DZnp}smW=> zvURBKtJ-^^i_or$N{`~Op4h6kM7E^1G_jcO7U@iIH+{73-+J*tdM?&_*A~~})85%~ z>H4als=hMs`27|DpAh|)^_t~EU?yPZUV3-CO!6qfK~TY8eWP^NekXb~7FAL%f6=kj zj+1TWu?*C?c*(-w%1*`*XW-UL@9}&ee_eXeE7@N0;(3G6SJs#Bgmz-t{7fKE znBl(yvOaO`Ka8uaV;nPxEg&zf^qhNLrQ2m;WfV*6x3&@tOQb7Nar95I`4)FiH5jZ-04rFGc(vv*}Sg-%543tUWUO&v||jo=NS zNlNOP8(3`C`V{-PCaQ)Krr(0~g$NaIi)tGs!%scz;KDq|2572@WD-$nN zTvZkpyR97-&E}Ek`3qU9Oly1wVh1Rvfl+1cYg7+zG7=T%S0wDew7ohM-jw0#q7SA; zF{IQ{I1pUcqZ6}6oYCAfA1ZI#12l38v^8V&A1o_E=%Tj zC()DZjK=ZN&eDK6MJE2bC%e}>W@zmvL6736rz;Do&c?4rzrw=VUn{5sDs2He=BbTq zfKL$wc>NhluJ|-JfSI$Jn)7m9<$;7lbS7tFhNr#0-eOs-=gq?O_v4^+F=kWO>G>>O zdVB^q_iHV4@3n3Fn+Fp#`=rVw8jRoyeBDkhU0bRbd&wx}s%x~dio=SY3-ao&)@|E@ z($C(XIS>ch`%%E*u#K#rCm9LXmz@>PF=yMqWMnKpKWfk* z4V-nXoxGjQ_YUtXDLXsr>MJWdm_-N^0}H3u+>4Hxw4-5x9H2rn^MUj-}0*nyIZsXCFH0K$9zZ`)b z+EDgOvyYdwM3;`250$t4nKkD&L;HhpCVd`szJf)Ib%t4z%9_^Ap24c$IH<*BbJBp` z64#>ItlXsTq~M(BgzR+eZ0PvCT7L^-Gw0H5)6IGXGzKr5S}#&BA(8zCIfqY$Lf=gw ze<$^;%eTLGm9WDg4N+&**34n`%vLLK$(ji}w=1d>-HCNwKwKc1=gDJp0XQCb$j9?! zy(O{Ek?rc@^XWHAy7DUvbx+0f9MG3y9(6_*0*?>|GMF{k z(f7QmC9A!3Ez@Y%#NwFCjm}d-5XtBHRr+k_vNAxSE8o$PBj9n@*>dt2(ZcYi4+nsM zg6qNeV)=YJ=#c+VzA=S3=6}c3!44J|m8croAE6QGPW$!w>*EEum|iARPH*BMy;an3Zu%Ly_|ibdCEEL3H#{*UL7|eOEin06Qh;0ow?Pyovh8G zb>^zIalr@UtC~N?IQv*HRvm?`&m-g6U!IR`Tz$-Cxn@~_Rgl_%xRy8+n-=k%zmRZ| zD-w4Z&zrR0lEdWLOG~Lx?r^45d(MuDnyI0V&93im|BeKDD72*Z=4ve^$*h@R+viJ= ze4i$NtbuB(3=1-AA#kC)c!xjHdzC+c;b0*SCk--M%cRZ}?zcWSy2B=62vfQBeuRRIX znW6RbNg2PCpB96E=9&uBul4CFWZJ~LtnEY!^!AEf!N2ID#g{iGe@;$Sy0qSeZ!7;{ z`s36=u$H`~w=uWS22j|Ys~!=Nhj+5^VqaFipYKsWXG&}tPXRa+F66~#p+NHj~T6m@Rr#M0M<*prWOC9JnfCbD{T_#w1W5R;^ zo&mQL(`&42_%_@V@5?FK-3YuGX+zW}hmOK#>t&;C)ZThZIa zL(uqkJ#<#%b!1O|Tp4-+j%C3C>;WA%PNqcN`U_iUM}Y9K+3!24AVjAinC182UQnPv zr!8t?0QoHBF(J7m7_+alu$=%1DOM^tyRXRJf-NL(2)Z~= zu5#SFC-T_%Rjf`Y)&m|*#N$Y^p}?-I9gm$@>$58;FPeT(4pAveJd~~|d_Kb+HlFN> zq`HuB+`E+g+0xlWTqQ@4p&wJ?9Z3Sh0#t**1FH-#mIVJ4^~|epRQO>5nsz2mV~JzhBP+lQ&K`~@QcX5dc3`r6sz-xbbI8i3 zWv_9|YUPOV%yra!d}z99PS;Z6nm$q@^MsYG*CX)!fajb5l@TQowI5px&l;6UR3;ob z>^eLu8bi*t?O7rJ`m?AoI1w1oezq!thgFsqtZD0fau83_aH=@hXIibU_`OZ#xC@bLEQ+sOTkY|-Z5TOLW$Lqee z@CS~ zNay$aD~X~<#T&!AS^ygngk=|EUl1*ou#NymNsy=%lFtA_(*tW1_daAkkGd2#U8pXv zQ9)%N!j~`1E8t+{W$%yOge!_C zy?0`+G^Z?3P=&|@iQ+8sY+TZ@(5uh}AOtx0p%M%|Y&(orENfzT+(Enji`$;Wf-{44 z!<4i>fisf}trfG?r1i~J%k2V44UG5>^L+*?33?@zHH;ti0wpIgu+X)T?Kkglgt4ty z0`)^hdqw`H$E@;Dt-x12IFl*CDrY>TtpRrji0$C6VRmv|6ZYe-Wd6TN4MTQg=ju(p3nkG2l`>UCW|ynyuIR~FyE0ix3>tY(7!^F%iT1J6cEfx@xYCuaW?k~# zcuITq1;)h!PA3jqI%Wps&2r=5)`~SGo(sXHTj4!$B4OUT<>Uunh(53P_s8#S5z|!i&U2}{Ride!Mzniz3kagrukBZzvce%H2W*ZyM zLfMovI2wiwlCYlFCMJ zPeD>}5JvEd3I`*QU3(Cqlc$qYk)zY2I##?T0Nj@0*Fne3tLDHrLB$X3b4FRd+ngQ! zPXY$c<=m%POYOh)gluya4M&ZivfRctR`iCyY>a->yIR@)Eir(A@VavU?OOeIG$eGj zvb1*KcI6}f=M3(@{eOTNhzb8W#nFO~SmUPxp@@zBZ$dVDR(eKaei%YRLSFk{CfrJ* z;{Qqhcf?0*=ICh4&A{N|;zIAjLT_Vl%D}|M#l^tL%)rb{_jd-JgPXOZp(~xW1Id3d z`7b`Aza5P2&21gcZLA6Z!Pn5p#>tV7nD`%p{?-0N&)=@*|1HVd;Xlp#YaqivFbque zj12$c{g;&YA5d-ubJyRN8lvV_zpWkq%HU^W;^g4{C&B-P`fri{MymPWq^#`!gYw@n z|4GTq@DB_A&7%KUu75)Rt`|QHFT=mqo*$<9fS48pL=Z$uR7k}Y^fC)l2X*CX#7FX` zE8L&ZpRjZljRsq0aW%m#pQ-EQMX#LUh^%YpD0%Ol7S(r{I~yghM$GFZ;;~34>pd$|BL^B@#yWR%m)A8lrR8W ziS*S})@Ej6>dW=+V&rzGY=>|Oj%lHwrlz(dV!`pT+z2A@c~RwVX=oafx5#UXCqAWxm?!MB)8<#DR*IGJ3?-|Gz44dw^nA&-iB@@8I^@M=w@fMO8JZ zKi{aeqdix|!v=7(fFc6%I3Ob56M7#Gvx&}!DNN~f??|jxsmGw4%4tn%BHOLYEiaJb zC${rJsE;G*UoCMwE&Tg88NW^|=96=?#>3kc?pFe*JpJ-`mXGwTfUOBlrcL!$B zC;q>RrERZ0U;*DRezzJQi$I;BMDY($L1yQ4)o6=fs7TGA+znoVq#^utFr5@JGsb1JOt1 z2XPWlQ2zG^04GN1^$k-)Wh+F$Lnv!^K7(Px`#PS zfqb>zB;w@6mUf($s~%&UDa8XzBO@c~d_%5};sk>th}3)ul;MU^7DNg(pm{TjM#Xr{ z43&@NR|5MQ`QLX)8Of~PL`9?k**jCTwz)Yun&%nUm+{EK*c-(WhA-|KCpbyD)vqQFjd=Y~L z4rw4N@AHlGLsxw2;C%zp_CE3W`HF;?sdwethUGHjckX^x$9_hT>)X=FU2m65e^0^9 z)V$L*#lOOrEi4Kn8`}_1O_y(xzPIQy*HV0rv5h=JrT$%1;Ub~8?7`n?*Bd2~E0wNT zeE59UwW1%$36rD1(D_8q+`u4RP*=-1k{`3vKU?|92*NOsPP|SRx7}Wc~un^9pWz8L55o-L!$WNMF5Yr1W4L@*W-9Y>dP)mdu7X0 z-HA%&`*RkQeG@CDsdYbl=jW>MC5NQdR)Pcu{XoE8wBQ-lrhqs9zE;btb_e@T*=naJ z`(|X6l5X_n0uSTY8ABSC@ZECC10vo&=n-rgV4sTC+lt>?k`*F$p&AYJ?i0OuUS5D% z{Z&!0QCsZu?S9mh5c1y~Q^yAEd4~mW&#(_uHbYOBN_lJx8Xl%M{da^dIUAnEvQ6Rw z*ZnggA|fsf&mj@Z9kD%{NHiu78hDkQ@!a|_yXeEnCL~0z?Mw}jtWH!>wtZl#S|8lME#0&ZtB258@J>GETSZE}cY(=^o7d%Fe< z`GgEd=_DTvlh5&EDT!HCbk;abn0V_RB4%P|{Z! z6w^0Aw*b{^@h~yA64T%B9(0fwAcoBsH627i*@p!EEV^so!nRKRmje-k?RSiNv6LKa z#q=;Dib8W5dA$sjJGI&@vzk0|5HlX7t<+hJxL0Zc;JdXOF*B&TUA9tyGg*1aL59jE z2Yq2(I26dd-(of$Fs||EJR@jtN~u!4X1;x6+a8BYA~CvOSbZ-sc&4g*L#*4)(9P1MX|S@O8g<5Ft3=it|UPa8RwUin9ZmDv5>iB>5_rBplxF zhkV-;2>&TCU*k zb78JlZX@uefqeWjo>~_PYtz$DM;YP|rDl0a;H-wU6pw_Q1b4Bb=@615w z`H$yGK}ALA%uHNFw%nM;3thV~ai#hw*++bjUHr9@L?xE<*|`HHi!OIFPpOZ*TMRa% zQ^GM?Mz*WUMi68z#UWBpWVy59n6TvWk9Iejz~RLz{6CXUBq~G~n^@mOe}I*{7{8BX z)ZWzb|5{E&NJ0tp79_ao>8XpB%NdpwKHt@Lv92~6$$Wb+*7}Ds~gVDWbCoN z!UHUBLi6{H%rOQkyLR@r5uBx?ro0@8t}l**_bYp9?>!NZpPT}?1<+?^J|vBmmL0&7 z-z~V?mE4&87K_|Mw;oaOUv?47I)OIh2Q|BUc6_@gG)IEY{rsNq^AfAost+0WlQgY9 zuAH5?Y{el(L1~P2*16`en=5-x+)37_E#oi?NK#c6{S%hxxYBwt2f%g4F1C=iJ zL*)wk{vN;eC|5V~g5Ia9G;HcS1Fg63dovntpTNxv)ZRL8T8IxB$}E2>7cJruh48*rT`gRm?(&qc38+mDs0r7AI`n9|Shg_V`r=a>w zl1@}|mm1-1HW$b2X{hv`sEWGl1ItP*Ee;Fv`z1jH24i){WjEuP*1Msu&oLR^IgRE+ zG&2G})5~t+W=c;CtgGFax&Sj2r+E2T&(&R!c1O}{aJ<;p*#G_Rsz5?i<0f7#siK>o zKrQxIF@AU0c}I3}HzA(j#@)Lzzf#T)%i-6BjKOMP+|1d`%f*FM_wlFb&UK2tF{{*M zg-r@Gw~fwiLiO6BrR1h3g`@^BY1~xU+O-;mt))0_N4cqHng{ zkmn9Y%(1ZWi+LZ|KiQ~$fYC|M7_HH;np{1*=e^F`tv;-J>eo6lj5A!0G(30Gw2dKM zl$Qj*NL?^1sR$+D-d)&)~kq{k$W|}QhBe{VqK_vM=>YCG&i%)LKg%g zN~Hpz=}I0j>PZ*)bdATT&RSCwVCz0i{=9N!%JCL-1oFKh>^Tb8|8@S3r=MAzYTe|X zNtGW?EM-RCR3%&Eag(T7!J%mRUu~!!1Y;t9jF62GEQcFsA(0R;fX;BfL!%?LpGi#V zA))BAMS$@-r^kV;8c*@Mx4WDxRa+;QqBro!ZlC7J%Fa!&YwKiNZ0;XOq>};o5hQSlc&>DF!>I(1-fB{BZXO@ibz$Oo1DaI>|zc$+*6``UJxeT zA1o$uNJ(jZ!<}_l3&gvGgLgk}@NO4@E4FcZ-VYSeOOkB%Eb)q##{qJB{FvS|5y1ER z0av3c#B4S`OZ^_5Y2N*4k0V#FGy7l*EG8RBW#y>@lRxaJA6`=9#Qv|epVGO;#?LpW zgzq<19lK=VsJ}?K2u^3mV1>LWb|)BQm`>}GFz+@;TzzzMOI3YC#$_k{1ZCYml^b0M zs|9oevm$V|7_?K{HDpXu`fHbmhoR++j8gwx*U`(*)^-5Oi_}bjyCX4-o60N@cKTJN zkv6WZTj~t$H4k+@qQ(xj%LMhigpP^ZuKJtt{9c z!Ct_1^K`nmGrs>zmKZ-w@zreB@W-TTjP{pjU_@JmnotpY02+u@S|h2SFM|nrVyPg$~NUf#**Xx=@SA8^L0>K9a|{AYd2hxV)+pe zWbvz#o1^RI=-Md2$Tb?QU8Xmtx2p;3OPVT)+XjrM?Ku21KOYjp=K(#f8^DLtb&sIc zW?w}#mQfl?h#0>WWiL8O<6Qz78WMfi3t4oiQp1d@#7UgV#(-BTv}V>YHW?z<{NvJ%m8AKArZPY?cgLv_K4-H zUQXVJ9)oC>+-LS`&oxaY<#ps5z!=y8TQVO55GW|-#R%cB9I4?6^(@)Ay(t;l|KLDj z3SJ(Vf@^YH;P7mQ&|_BQUy5% z9oU{S+&ewJ{aw#wS;?XK*qODYpD~vlp5`>fJ%ZoGc)b?#&L_lqmmotr*9V^6c(1+z z^L3VFT`PTdgJeYBptp1^^=;6>^*Mpz_LhagaMw!tc1`_jAgI!ElmEawlKf33E5Kf{ z2+?TE?pY+2^}$d!5qPocK!xV<h071BKgu2zZqGUVorN;O+x}uM z!zK0>^1yk?y^HKNO*X*4Jgq^5!2%`40(C53?!N;fZU^GG>LiPYx-=o@!j6NGNW@3K zJ0etHQ_@q`Okma64K`oU4#sxbPv!_L+~NIrLhh$TYxcUwe@bQD`*dp^(Azk?kIJt!8cy~tMyiYLiB^p=2|>+prAg5~MJ06H#+=IN1z90V%k_nt5(W768(<*WYb3PP(Ch0Z-LnDT-A`rAjmlXoH5dsV^uqddtC*~h9uyG+B=|f- zcWAFzL7t*VQU5WvCPfeY%Ocfv>ZOwr+;o`F5!vs zjbBWawb0#nzfMz}9`1l%jq0li2`Ofg<6d5o3ivwF+k*XDR9Fuc{F|pJ%;K}DO;%w~ z5>93}Qh&N=gRT7zEx0%)GkdV@QfE2U0<@HY@Jfpu*&4{{Rqy%mSNGwU)qN5psQW=( zm6!9|4_ISz;1}cFw~rt&ETuJ%cKP*9K*lKT*fyray8&Ck5-MQ^T3C3 zkURm;x-E5Jxn4w&CVDD!e7Dfa_3-)!HQ*G@SJ?-TH0Zm!mhqdi-W|9D1bVAu@93Jm z!M#XM?_3(XADP%F6@BkbPfc%RD17Se9Q62#tAe*_m)_2Ai}9iSVvv-Frq3wJ3Cpqg zAdQE|5sjC}65#pTDK|Kl0Pzn=ZNIsiPk(Sbp1WQzeIybBS?^x`aT+ztyPfI2E}Jjk zp)W7!oGW@?tCem{!7~|scT9#4d;C5T>w#HYkHZD8dIzL*>nQ{dupZ|kfderueUpq= zLjYhc^=p|&L+oN@N}oT#PdLEOKm9XL+}wjw$x#V{sbv37aInku;ce$`JV2f(ITe7< zqeorg0Z)AMfq-7~33`yDF~>SZS;qRIuAvgS^(&l+ziB{>d4mO=ej!?B-F zC`W<0qA}HP|FjYU2s4UUgS3t>*hn7-xJ=rqxoZX%UVD8~?l_UF?gA2~Au1gfD*G6N zDLd^`r+HB+AHXdAu*?C+!2&=C>QqkdYK&{+MmO`U9i}>ge;%g7_gYy%czIDiPc0)p zQ46IrfZKSdK0#sUSmw&r@L0QNwgk7$yo|K;1zw(>TQFUNeSPyM?`lg5`qf5@O$yAS z8|eJw1;MFMk*T_;^5;*o%05l^RFSjfS8;)`%R>r9tysSW ziRaxbimwP@UyON>Dz)VcP&d(;AksIT)l<5E_~?1=r~SEao?N^0S!tXi6>_BL(wJ)i z>)NNI)1WTOSJ7B*_((p2{Ja-IDy0VT7C6&2D7z-dM=6`(GqWQ%QroE6HcihVl;D4dX9)a8Ou6C=D)woj;#~rT2MHamt7^a z(+GQ-f6q{XlV?6d>8m?tou1_$D;95vPDZv8HuIvs{%*^AjmtQgnaFy1yB>a(io73s z54K(x<1Ls%B*T^QvJ=%CgQ^2x?S`?n-G)GP$kHy~(uc`vf?Nz2Hr#VUFbph8XG*f* zWnN+tMA3+CifBxyAWJ9K>PgoOODl&(KRqpLLr`Nk`MDA~kJf_}T?_zePQdTS$>Xrv z2suHg?~k_Hh;uv@Sbd!OGJI+bgCcz>a?`&`If8y7ot>o|nHx}vjd{<+Jv=;2@TJh4 zXP4$l^0yS@V94YYL&#*IwYoI0ZXwW?Gv8*^3ozZg&Q4F!S?}J7vsT1m?jkzk(1f!r z407RdR}b#3(Uu2(4;DX@Qhq3H@@47%!P7-)AEVbWC{ zA!?++d6T>Vsxp;x;TuT;-NoHf{v6*c{%wFoJRC1>V-KTBS>8Mh>`yDtU2I7PyY=4Q zt%c4zCd@@Sfnyz7p)~fXmK9s%=g`>jH}}{9PYeuCw2H5ZlDXA+lj4=HAsM0Crn;h@ zgZ7^FDo$%73;NnLjoe6FjPn30ey>~}6FLpybp>-u%A&~R=u=Vuijrm2@jd8rWzbdTO(JdkO%_G|`1d$smXelnf^ zEO;wS_t~mBPiVGfT1!Ydyt{foBgZU*>MKm+W!x;D@M@Ui4VUsp^H+Gyd|}+t-U9yi z_BOy=UX4zd(bO%ym_A-LZM3JLd0V zy>h_Fcwe&AG|12e1rmw*bMO*+fzkV1gp<5`zQQ7bKf<9+n7&k-K@mTJ5r~8lppf?^ z#XGCt5L^$N?H@)O8m`F+yR8dhIJ=mp;U+HTFq!-7L9Uub0O;PoFpT$E=%}Ek5roIX zaj-;D?2CG_dn9iGVoqlsbW5an-x~f1!IsV%5T*q#p(UJ*amHG5Xp}C=Gcg>bXw6L} zQB}WkznE@)PN=_|#?J;cG=eiCnDtDSFYUwAt^O@v?WCQXPBh|OXjWD1c6XjoE z3c?+?W%7xZr4ouheQeHIqgz!mzmjVM&^UV-WPqk5+2O!wm?o0CyG+wIU$Pyvm9?xr zUm=CkRQQ(qZNM8pqw*M$JJ3Lbk8fB$hjL zXzsAz*8-rx25<#rAwx*|J;d4Yg?a6D0$E7^g3-U|lkXymfy_JEf35Sgi%!)lF}-se zp>{FM{j;GqwlOSFT(QKNr;bmNLW!NEX&)tOyC>al=D>E{eUoPv|3*|I4mu6?*k=Bd zsO~CKNDfUafkQje-7~!z#W|IQSp$VaBE(}F99?T33GWq!@ zztwh5e%l|M$hCNE#w>WxANmPtdv7*9AtS`f*mwc<@X|?WRedm3VH9qrBhRn|gCG?5 z7-9@BkUM%IJRLks=W*AIxa5a4(lBII0@n(!-vvo9!A+uWime)dv=tcw{~R!Rz5=Z% zb-19jc;XH0wjr)OIRa{)ViKxTrnt999n^Ucad0dm@z`YX)r6J84?8;xY&@X!y>b7o zAZtqp&HHleCCQxtu~*WXS8G27(b~>wJiVoiGwz(A$1)UV<0DwVp3KfwFM262qi9Ce zO@VPR!#5^4t8@AK1Q&_RjZR|98}VD)XT)LaXH?`80jzrUV0cT8(DhgW(0xsBY$_b< z!)&{fuzLZ9pxd!?x$kiFL_^vFw#M-A=CO+lbG8o-)a!Ncse?e2BJcDUd6^tlyI3Ur z@7~G_(s$EJfufDQxfSvRYgMYJYhJ1&6dLOgUnp}cL`GZqY=WDJ`P8mend8VK$0;TQ zkk{bHevyZsUXjL!_*(zAa$CHYAbD5N3>VL+7JSSoKfH|rnpzxuty*00P~G?L55a!L zS$kxnfDxkJF_Z?gf%-P2reVutKPF?5J@FL+uPas2S-{%U!80;4Q9QE)eE(HnHWU=T z@0Z5`d>5Il*Q{Pd{#MWF{z!l zLw#y7rjTTQ_# zFdjE;tum40vt5attt|##CT41NSstIk@w)^$C>Yw)d5U=5P1$?O3P%oxzpV*pO4n5p(Si{H!+{Lr8rd1YdR7t!-qAxL$AK z#9o3eQ#Vs2!pbj2TjxyL?_f;JIP>Q(mSSp(8h9c_^B0~mUTe`bGo`^bOf`Dzh&iB% z8FKwKf1n{oKvl|et5`=)ov|iaQZ!oQI2b6QKX59+wxYV$nD8{wVNO@_)?7j75`Zr| znRU0*mf-vKhK(SaU~_S#e3+eVLSeiW3_jnMokP{!_4#Aj@e_T)Y@pB# z!>5aF2|Jnh6j*2N3`DaqyoMAtjkVX^Ykv3oPRKUO%YCm$p~1LHn>^O@LIGhuk!j?~b^E%LlUq?%1?i9XNb;H6NT_3g}sb>f9JD~4wLFN-tDM~e! z#vwJpGi>%ig;le+#-9bmczd822ROcxB}yel{EYbWJ6T2@Nc4>9x}9R4Jf(XVKNGRO zXYb^F#2ak=^9|&chQqw_#kck-bK&2z6h=EJVGKro7WVP?kHzhW`PJW;?=d+|>xxIMmIP5vJA6Uc)~{}* z<545?DJO=C#6^nw*qH(F+(_|}e&nq%De{i}Cq5sL#v+f@IsP*5#ZINTjBs012aKkh zG_Ce06&fF{25P=V&y#(o7_5(pEyJxgUtscPIWaoS6iwHQQcn(Q>CJEmG5Wru2t24f zKZe$Aa)e9${SfPit$~;<+k8+S6I#eRLkc!Qr@h`?qnFKyB?;4`18JPjq~tLKbXL|x z6!wAt=5kh&1`Fv$g;$4xRvk}lWoWRl4x3EYsLt)g%+pFwBqq70#Px^l@00edsT8&w=Rf2T%P(+_ zQi;Qmua?u-TJQQYpApVy@5ZFtFqxP^#n(`x!#LyRwbbFE%}eVm29sVN1jZr)X?C zi;c;8t-9(lhoE=OUrQbYKk%j)M3XYzTQtH5xC2p2t`;&%>8@=duT%O-H9gmbNQ_=a ze}CI_K1&9SV`=Em)Z$VG^9J3BXKJ+^e4+MNcykAf|5tR#1Y2ux0l4Ixc% zo`3y?@-BT52Rqb`YvqCpR)^%>3#ev8^xy7S$&fsDBZw;P+jx_`)z#T(@*-K((beWWq-j80^k3nh(vvl92aLNC2a?P32Dj`gUGjk!YfY)D1N@hk`nprW0kn?L6MEn64NkjCw%I7Qc*yPlrY z=rYne{=n)ko--Cl82)6_rXC|3%;>7rzwPIU-mQJIPMcI4D+A?}|Mt(fk>b8LiK6mq z#Ag<0OimNpw6nDZ0`gZ9BxR@@xlgT;wIzk4B9etX6gz~(Rsy{yl(j73y~A9qj$+)P zGp18+GiLXxv|cK~ zs9kmWKH;qo#Ah;w&;G4W$~JvflnI(oYw9>&M{Zrwed5D5c zQqvRD5rs*6{~zw&DZJ8VYZvZL$LZKkI<`8tZQJhHwr$%pwr$(C(Q(qjpLebATkGY2vQPH4 z59gfEtQx9ORd?ObsOfRxmz+Xwv9!o8!C{rf^3G|Sg>cbFm)AnqM#VSGoUI)C|M316 zy?IQm;Vzn9nO63d-}r)M_r})Z9J5h;N#Og0RJ_i)DIW^!ZXrEY+F^Q1p30FxIXd97 zB%OMdXNk8@IbL1)aR|;mi5;7T+2yZW%MOXOm4)nGBuOiM%jrkQ;Ym)ZSE$r~;9Aa1 ze2surmwW>Gw(DXJFtw{nb+p&4y;z`|%1@Jdrn>d3vxJE@3x$X%y$^@iM;YNopuh%a z3*3AsOI%YADGKBc^YG;y$0vFyLa7|Ys+~Xbc(>Kl{tO%`}338OD+(Ros(#y>Uv>8C2a0cw=lT z!lXR|L)<%V!)uh9W&S}m6H$pE6sM;b{os;E1MYe?G;A*!;Y=FNEm6+EGcA=+7Xb(% zJ0DSYOAHk<1IQ3Pi)a3nh~Uh=7O+|o&vkhA$XlFwe2vp`)x($?t;*Oq&GyvsEp1}H zycr>$Gy=c_2b z$_%m6XpNEU(eWk;Pu{AJ#tXi-lRbd^MBGEaET>tMV56r`WzUqlH!!e!@pl`Ct`F|b zR-BVo0MV1_`C=>Bx#xkm7tJFa_gh=e6Y{zN(X(_bRPUJ7&K)udesfsM@blz)J0p0L z(AR$cSMSQsjQ|b;{u5?T1ki0lt~W_5-Ra_#%ota=J-&%Pw?!R$ZxtppKIHOI>R4%` z9I^y5E+QnF4?ZmoBAaEX4lj^^75fSx6Ihzd{)d&eU*z74_M_hkkFX{jS8ihZr;ad2 zUb=q4$K45xGSTndi-U}ZFWkSu+u8lV0PO0m(MD0u#qcqe&7K>~iTWmT>Y?AW{H)I* z9-UowJ{T_F2ua~qoi2M(H$9_9Mn=pxW6am|99-zg0aQAM7JWnmj2BSM4-Z5#@&$aX zE)I=#)v<;GVi@!LM0MMV$meqvjZKquK%NAOBI zJxKB-V>9z59EH)9`^Z0uRm2ln1$#8xnSwL!F3y$w2H$C86#DFs-3@*t2G#|Cda58F z8xdeN4O?#h)m=zNQ-{LX7w`wS{QJWez+aZlJ(AEL`v5Hby0UvJlF(oVs?}aK)g4Jq zrgXUR-pW{l5Rpe0QiUD7yhMY3xwPC=ksUXS;Xfny&&=616<{?%v;wTys{-tU3cC86 zob&g!p`cXEr_@+3q(*!T(hSxtgBg;sr_kYaX*M8Ldj%*jmfvqS|3n40WCbdx4sY29 z|LOfV8vGA=SS`Z4y1YuQ+_ob4cT@hKWg~^Cq7@VM+x_$R|K%TBh<)&dEkcthjQ@^e z`7g<25Oj3oV?hT$_?7>m5dS_dx`0>WKSAZKAkipK{PFwWiJwjQK|3abj{Ijk){%Wi z5vnKCs!0Dwg?L1}2#JXcg;Kg~e#f)>uQLBub4m$V!i=;|abJVxp9KC8>_aOC&i`|P zKCCB(f%VVEGm!_P3^$W+(_sBaS-fLEtQY^C<)4i=RS2wtHecx6Z2Qk@6Ma~Z%+&gy zjh8L?5exvypSJt|91UV0!phBTvHYW~JZT@nppdq+Kl0Df5dR@;L4Nl?yU2%$sH>-# zsed%#pQGXbxyb#0El`X`;?$`!9mqk+`ptrQl~P)AjkEBU>PGNNqg~HvYbq4FR~W!! zGB{H;%?&*vBhyEGSH?hjISIB+m(R%G3PHY~F^tYv)jbGL(rO>SX~CkkxTzTGfpi9o zJg(IKcyL_{Dr|5$9+M^1rt~W*0Q=_JxX_wiQf|lf=OJJT3KU>yMyN&EeO!h>s6i>n z$An!g9^=~l^)vMX_|KYxC~Q6si|P4QnC?tR>$(mbL5lhL`Qoy&crLU06+BBLBTzd# zyW_m@M)Spc^Ht2GrTK}Ok*{aVZb(m|u0LLYrf2gQvl9t!46t@ai?8L$tj}mMea4Z9 z6_GQ9HQZz8TIXz;$8c6;r{yCTqZ|l*lOpN z`?<6L1fj)I)@!{FcrxkbIdT>#U%5#8cUn+tT__`D{G$6Oyp zL?9waf5P{W<9j2hDSW)>vE2i5ytc8Sg`j~>;l*gW7&SG2+cI6#&w`#NOQP&zMp;8~ zN@&+3Dbyub%rn!gr>D10&#OZbvA8yYqoj}oN1>)Vj8LNlcm*vh%m68KSkL|W1}#*E zNrn68Y}W(=kv>K#l;w24^Q4WSMjR|h3-3!Q8&xbl=f>#^M`&nl%x}W#;^x-7yxgqC ztKjj9XylfgPD*nEe;4Aa&VHoGuGV^Rs-mr^MZObokPg1EaaBLUE7Xxn=+7D~PUfu~ z9Cv{6?uCd@#hW$!kT(fEWhcLQk^E}?fSFCQ2J&ZPe#DFkj1RZ;p7{=vXn^mC7!SK+`hPu`QoO>!id&H`Cf5YDawf`-)K5NDU$(p`uMJKASK4fWMRw8x!L)brXf;_;EvZ7s$J;cd^1xhk^B^gQFAl5 z(J?~5C(BM`hzu7M6Oci$rlC2lV3TJA35_r5K&dp9o;1f zss#6YgIXK(uN-0s3r0%Hlg2?sWaRAZVwn~v;LxSj?(SuXhPlf?zMB!#3qi(+*|r>4 ziletZvJ2R|{(ys6$uG>sOx!ZqJNFXr%wxYt}E5ke>x8D9c29r6dQtNZ*7W_%%&R zN!>@-mUKptBf%LTryx7Xd065+8<&K7!WZKkdm4Q&I)NI;wlh>D8k4*;Y8^OLoMkZ7 zyONle7wVLmYW4QDd^63=(MxG0;54SL&tx!s?TOStwZGrm@rbr-7I01a{(^NPb{+G_ zwbYsh@O{yN{u(NsjPgk*Y~umAzqj~Yr<)e{S8!{~aX`cZ(F@1^-d@-Gx+Z-iLuyGt z2pg_^t|xvp!dsCV!J2DN@d;MC#9WD>D*pE&T}QDHWs?^~Rsgsu_`@J5wF3NLyFC1b@7rZnmIc*JcN z@TjIbK(01k2qs!nTM;fj3E{aacnNx>N>Gs*Thn|a(Q&o1>O~o)+39M~3?3z=l~~MN zb_3JD3MPGoJkI!LaHe>j_Bm_JW67P8KZYN*p6?+BahfK(qEK6WAg{Fkz8X&#Q|Vla zPC|)jr>H+9^?Rjflb4>;m(R2Pd#_9FhxyaZhI&R()a_vUG6JKHd}Y)VUE1o61a!jJ zT82wB$77J)enPq?D&@a)35&Q&AdRCg`YMu z3Y4GsM#%c(;@l_AhsP)fQXU1{f`KFal~_xcTi&PSd8>cj{n-W$~HnZLiU)v!L<2YwFR6!DojcQ=0FZq=U%i z*(HUAUkf049c52&UarhveMw7(!&%DSJ;QwLekCT(-icsQm>z-{7L$u~g%+HiA`67H z^{a*~J@&$~q-s`Bik%pUd6u5Q3xs?l@3Yhl=&i@fsUL8T$0l!a=5M2~&VBGQ;J}_X+b% zpF)!|$zVU1|B0okw+Tx9Ox)v|cv!8A+O{FJxv z6;UXuHPpKZxAbu9+FXZ64p3!rc*LW(m&bU{ZxXiyzdQFZ}$y#*Jr~M_S#x5`}1MEzst>i4NCgLqd zvp5qWovSU#A7VwrETJr92YCfHM$=taEyZuv?O|KO2SwqHj_}})M_PG&MwQj{j?ae= zYXiN~JaP;?W15j3>kTKen7lRoR^CQrY=$kSRAO)R*{TD%vFm0P=nQXEvkXvNI zPXRlo3-R)YqPt`QvbDsiNwIj1R{M*R3&3+6V9%amYb`DZI%1Ka(3ysKo~LZiW2q6; z7^+Gcj^7~;`s$MU=2}kZr7l*juCN9U1i#JPop^zi6xm1_{Sg4M5ZfBnJTz&KT`aVi ztI+reNDsYa?Q?Nu4{FD8L^ZMUZ2|yU2K+hrrb4b}_2|Yt@P5mi*-10q2nxw={)Aq5 z;_$YQ>kX0mJ6uy5A%SqLh|#XVGjce%%u4oWbUH9(AYh8Gsjjd+8V)1+I9K=3JdXii z9&@d^>V1B& zy!!S;S6LuC7MY+l1L9vrPH^)z{SWjWF6=ceo$s)MLOk5}?WC=^?V&`wq`A!l$P6MK zt|SO8Pc|px+ZC>XF_W16Iui#dq3#9!ydx3?Q=f?;=F{V8m+DyA;r<>L z8^UZJ(B3sp#q<#mvGVBK6y)YLHQyMUDNJ3rvxc85f2^UBAzqxn>>HYfJDQA5%S6h5~*x$)U$>&8pT0)-j0$0)n# zC(T@r-hxqSw+-@P%Fq~Bj9l(iSKb=aEolKj6~$;`1JSHg2#dA*?g?@D>H#{MnW$h*kTA^Rz*pI{g$!v`PzYg)Z+|mOk{%*St5F=wG&=n_ zdTrhIyjcM!Fg#pkv5AY=vwjN^x>OLi=OxPZ(4!K7Cngoh`5fLUNy9OtZNfVS$xloQ zENcsG1Rm0X?bCwHc&yl$IT2fdLWngFC6?~CJbJZ{NNc4CxIj>YFj1w_s@{L@zl%cIhqEhvDDSWgD+>kY%U&d_x#yO zs1Q_ba0~sKC@iE54Rz@qks5t{Ck_MtR#XLeVo8%T6tVu^S}pSN#JB-`tin=3&D=ud zz_Kk+N}lWRWoZ)$%}p9niO2I%!i(Rq1Bx6q@pz*s#peo<)42f1_o2YX;TidG0>>jU zFp7J95K~cY{&gwe?c^tB;)69mvHjN4w?e53!or!L0ek1rfW&+$!$g%RS@7IFwKG3j zvuLek@*?>?v%$3qf=?OsM(cWwlYJ|Nt~~?E`JK82(LlS*@PtHr8~l$rTvIiKdmAG(i1Jq|s0ziX zuwu<=PxHW!y}l@c*qN&nm7j^UiAczOC9%e;mbRocCRJKk6U#aP+8}qnAhHTSTUhx_ zgd0#oc1Rc-MgX51=cG`Aq$cYK4&_+hD0mrcS;bZ@&y+E3SVlT84t&;PAJlrmcb?Zw z;|i>%D;zqJUF;Z0oZ0Hc?*S=}=Mj(7-X3kslcy+~?h*nlwn3Vsxo+Q0F@<>&i=Z)0 zHBgUgX13BD@3{Rf0a_9=h1sa2C6B*6!Q&`fN9m}Ttco=} z3GA6Hxc_O1aEwNB5h_os6I5jo9VQRWrz?!pZxp+}I)L%~2}A3jxusX+BP2$IP-p?*)AVxAlAeEUix`t4{<8&9-fcC)FAP);3PVk%Cqz z^z-Icnqq{B_N~6yWGO0MLPA3KTDk$&t*PkHq(part{6+xbI)J~3fuVCmIguYA3wgX z<1*F*N>PUy>kF91i-h#ba?XpEswuMSJjM2;xvtI0z9FXLm$P<$5PG>fhwRgwu`@Ac)n5ky zNEf3D_iPDnhE$*fO=0T&R;c|MTRUZ5KkskpiTD}tsl^oc^ zEvWID%J#N^s95Z=k2)x7J$2Q_^E04KcZxJoi3EQ<*w{KCtP-vEQ63fA^4{D)3ysP% zdo5la8>#5PO$7~r6FIY7e2ESbd`Rso@G!PNUqL?SJkzzk{ASRgLq)iP`f6g@#OMh{ z!x<*87Bn;tB%2cX2u1$Y%nF2p8FwX(LR82lJSO>@hNz$8 zj+u&#LcwxUyXty*Msrzl3}z+MnE~UE>F!CGke2m{H3HhUG(>W8B;U`?@bcFqG*SQ^ zy=@ro&jMs3OiBXOZm-`CDn18iVtMN(6b!XGL!i=6Y`;7~4Kc}>l6CtyboQuL`)P`g z&f}DnfKa_S6k}uv*NZQ5R?dVqoM`>PE2tls+8L68S$ofZ+5y@1Yzx`K&pkE-y2!^%tm6p8F$*qama>>pMiw+1ON@Q7@ z1ca&fU&0f1zyE2ohX}_Kue4ZXE#LBbmHlZA{UAJ=RvLUY3NgZ?(lUQ0@jlP79vK;# zYDVPw^i|dQ%x$av&c@l@A63fo5%`M{XCP{BknWQQetnwh)tWD5dWFWcdJtps@=(7z zADROhos?ExH#wTq1P)-cyL2KutlGF=tj~PhLJC_{p!0rRbaq(H>RQPb2ECxz3+i?U zYN|pT+N@ED5>$TD;M)jI*H+6l;mw%KzG>JF(UVJE;BBdLDIvG;&o0&#@i5i>on~fAN~-BdN&MMZYNneg zzl;nS^cnCx>h$rzjga&XGWgPwJSX3n7IpJ67@zg64hBivLTZUS+bQG}QS=q5?`OSI z&SIm~WddNOu5(!capapZESLpofGj#p`kz;yR6KZOjfQ-P%2r^Htd0DRaxhpur+1L+ z27FZK!snpm6cp+@xk*v_G-tp#T2Y52f#QnySx@AN?yoQh7t&@8v|`C8A)a;L*MN*% z16XV{I;$Lt%4e`K^L3*v*FEl7Yoxc!tWgXFJ(c(BH6Zk)k5GJ{%UV=~&P30z6_avV zSDH^UGE3zm(KOq{SScoVom~mkBm)&Oxkp0;bNMz5l1t!7q|Mx~gYAuRVMR^$&q^Qm zjh`i_7mK-He6! zXI_U%r9}McKz}QdDEnJ~K%&3X0+#UM15DoHp>T|VqQ*J%@oxiaGvJGzd<2>Nxj1zC z>hAq(T9w;|jz?zfu1YH+8cJbw%uyv18sd4_Z?3uRDIPg<&L3JZ;q&XtlSPrr@Izkh zOo}GB5my*MZ+CSWHoVzo(vvn@FNX}1d(hEu^8}-A{v?>r=StpCHaR%p04B58sEVHw zY+<(GtGXPh+lJt_U`ZG>7+A7H>9UBD9<#5h1P;5v!rf4i(XwY+DAsqNIV{A%r-<+9 z6s}Q{P$c|OTi>?f6oZZ^f>4t*a?{TrNS`K6Z!cjePH<#n#lKqg>~t&w-^eUk_p zPm<2~_EIZL-?Fzs61nyJo`LrFHqr^P$?Zmmpu6Kjqlo0y8HUw**tLDv2<}?d!J~i- z-W7SFx9hrtJ)Xu?^fXVCm9fKchncuP+ZcZU9 z)85KyV1xcVEn&dp8E-EZ7ZfthzG$9}!7X`x1r0L6UO33=dV%;-<%5yVOjcJiKJt{h z*`c8jdNeQab=E4iHh#&#LwlVcE;~u|Xx4m?@)hpDFwjqpTb^}LI-8arqhcMj>!C}{ zMY-d#oO>tT3e7d(G4=}Z)fB=ec(Pa-IYiB7F#ApG@yT21lzYVPAV!0aANif*`i>7({=Eb8lz{p6^fD~` z3&wcmOCR>*g$B^K7O?Ksei-C{L!0X%h&>a^q+(2^qt(IA;q_)M^l?u1nCSE5`;7#h z2Y{i-@eH(2M@3b3zti&mO)z&%zK#`9N`J~~LzxEuu6+cW$juR|3r@ks8~SK05Ip(} z`n?f(x^ysOvM@361=^Z23Otj7L*Am=NX~R54=F7`qJs)`q%^oa`@vmP71H^4qb<)kFNJ|)`RDh z2;iod(~L;ZE0PlZo8r}a(CFjVyhgt0e*SD-A^ilvzH81hE;nXE_Q~P3ZlDwvvZX(F zEXJvV0olSEO^dyYt(5{Q^qIZ{5)vN%;n*5 zaO-)Jd&cn>GEI{x1bBIVBBZ}u15MxFkue^}HTAL=1R^`RI}BsY;Jk@o2(hK5Zu|*{ z6lprM!v)Sv-{+JZyT*?~1E!MdCb}MM=5Wt$t@^j&_>ZRZmdjv@q61`Sxzh4}=6x(* zCsiY`)r1e~&^e^~&xB^pP1MBoUMsW>0MR&3-dsW>Y(aV}7Wkm38U&WTS1mV~ofx?#Q)MFv&!;msm=BG+7Am7+G z5A?S#U`q%frNlxLD#Yf}&)=S>F?hjq04C;E47CTL_6kdFI+Oz)Er{E&UZJy>T#;~x z2MamqJ|>+7J+!j?exvCN-gAi##n;rNb(HS#7gn=-2R~kWk%%3-nrl-7ic93djV7AE zz;{RuoYs6hFM^1hlE3H7kNze?GzN#{!AuN&3}bAk%BLffRtOX2-|BGwW{%Nddp&q1T1!hVaS1k> zEe_4WAE}jTy8v2K&xh07k>+;Sy>>heZch>-%GtfLjOsVA9$^FTYB!OX$WY&qpvZW- z#=puV(c#7R9OshXk@TEg^<;Z<8Nz-rCXgIPk8MKDjS$quD=R2IC=W|7SOfhW85Y=C zo1rIRzZ~wUrLcFhsc^;WbK&)$IAI!@th%A)1v)pDkBjc=k zteqID+q=P@?C%X5?L8_pD)9))@O4CPClnnhw@}>UzC4f55XNZ}Tq2VJinD&DY-dAt z4p37+aE6VNvxGE>rP=C0d*+%^EB5?lD^gaTBqyqKjn_|OM-_+30VWp+>+4KF76(nn zK=L&x#sNJ`ZE5e|7X6eGEXH z8VxlmPeBP)86Z?f&H{vlB&akM44SE^ZHahSmTAhmO-}Knf)&OROIjXF(e3+kD`rzM zpbd758xam9)?KxllD}4Nh_Wh4(L7*`k4l#U;NY%gNLQ@TlUdc*C}yIJ+fOATzlz+L zuU4ffBkLMutf%RA`)cDRwE3u+Yj86W$XCO3=I1?R(>UFYEj`x^IQ zx=-UKkz_~U+4;6Y{hsa1Gj4KvRwet&dpqgd*K|91_nh(aUak5vW|x!JOQ1 z-%JV2pZaLwBOHYjFl{1WTuNhW&ZnDtAHEPDP7_>rJ#O-rv42PjGoX$GIxlT5ea`ln zr^(>gp4ow#SNozN6E~NU)!Dopbi#A6Gq-l0ZnUX1qGzdla84V%{XOiU;zz=+O*?Jo z2KMyw+-sAE)8YV^Es>Kn?y&M`#g~>9*2{L9dZN zxA@d257j1sY;s|7iSvD*{lhFTp*UYz#h}}5zYXc`^~QoFq+M7VBt3aVKUor~^sfGy z=z`|BnuThgsuT_@6~pu(3f`csvdGWJly zD6#+JlYdEZSji)BbvZRWh!|9w#%^W?&4)A2S^7ocm-4C!H5kzq)gW<2#1>vkCoL^&*#@u0*X*3J=K_SL}Y7p>cm9lpw3m2 z_17|(*)E;LdJZ{=$~6Kz5tUZkCXEZi@fz85-;2@Q{T6 zPGLJ%{45H472%gne9%he%Da{0Cp%$U)LUD9O~OS|5tGZ8>GBKwrq8|2c}^CFWs=i?(Y`a9mxJ8jjaJ*bSDy;k{WJ;6 z^zfS+{*EIJWiU=jdXb*n>Uhrk1RG?d@XXy9lXgai{*Zc1k-5=LUb@;>i=F;#xG!D71J zT)Ftdt{gd9?g{YG)ZCkzo*pfL2q^{ZZ#1>Uw>Ga7kH*9$*T=1w&?$K1Ep=CXGvn?Y z$RDtl^U3uCgEjLDYuw{fT;xco9ML{6Dj<$TpO=dr3;L^IpBv#x-`>#P>dv5$Jecy zk4+s5``eaTOkY4upMKG@JHZ0X!^f!`qZ+}Cgfs$%b(MQYch?;F*a`gVO0AaJU2lt- zikmT(+Q@tek=^CWbLON6BeCnP#?5CT{VEyI=Fj9KFy;O?;r`c0*7)F90Jad+g(SU= z0eT)FHfm{v;uKe5yiAM@C{6cvjg7K;#JB5F8a1AikDRm6vRH4miKC^ZrKTem@hcxl zg528S`PQ`jZgx11q@J$G%-r1OXFsaBPjMKvS$j6dhd(_)X}$h1uKz8?8sF9t1VIOv zOh7Cypz4%|(NS^8T#!)=HjAfH>fFY{JL=_eXT_51XmuTR(f&qo@P7OfW^sj-oU}lV zy##muZo*Z|VOPGqa4zc3-5fM$Y;aU3Ue&kq-{JCK4gV(!si#~mD%9GK);|ADKpp)ccs?_u=+KTtP(%q9{Wf3yx$z6)E}i(+d9+dSKmYQZFl#E_-K5MF z94Hxo<6_9Bv`>Dr7fSk+G=I}DlC}P4(7d&#YZd#yu^{P|CD2!)X7z~bw!c%lD*^;9 z+;(?FfBN^C<9~(!zgJ;cX6n}7TxDGH-ot<_{lyJPtT=`9Hg|Yy7a9#zq;M|I7i`Nx zW*@i;9XLhUobT!8YoL&u+?lnDr5YO!G8f+Obi)#C16sQ zQw@gBnGUVVZC71+cE>n-8gV>lO4E_)6Nx!`hRGLsOSS*X33>-XC$Lu`5fq0O83TB0 z8<5xR@?#aUpCT&;pdqICf4Sl}lVajiW4Lfw2rvZwiywTT2N=a>Z=AwfS5e<2q+yK~VXWA(MWi4!2VM@>9 zED;0hOH#Q15+PkNT_7{SMF^(O>a;nzyg6mMpWIvXw&2l5hUS)!%fLR#NSi>$se zOduKW&hpEm&Gf&0UQoc{Glbs4o$4p9TsaZLV#lZ0`Ydf__4xQal3rT^io730*bK(e z_Bz$yS$)qv9wvlt4vojN3K)UM~D8FbcaA zJ1p{ko z^tw}`t!k@lX+=jod5wD`M>MR_&;m)1hYXH?cIakEnq7$3St%Bc=G1*V0XeclMlB=N z2&)S)g+6Iq#QRbxg&xM0NH<_s#dSxsQY|f|?H`GGper>2Gg@7ol08(CjLE%b9$L9y!uC^9%L}8L#g_8jqcZ{029*lCLdA9xUD?Nk4m-)(?HGICZ zDJzbi*It??BDhB_PREdIS;du3@h%$A7y@@cg&bn`=0()9lYseA^4?SU1ctp=9DbFK zu3PzkJ>VqDxAYlbvniup3mXIVp-U6ECPpi=?E%xPp4fn?mvq~1UPRqJ zppO2O!4M~L=B!s&z^L}x-g3QhRxp!TCeoEZrgs~f8P!ksYEz7d4NG*$GF(WVJQrVA z`~-<8%!z*}Mgu`l-SAbX!_9(xXZAY%tP<78pu->kGr0`OeYKvIS;(PD9uGtr6&=Vz zs)CR(x-IKp62cY`e3LaOZ4zX&{!#?BVpwBT6cL=F*B4HyS*U57*=V+R5RFsKi1(Lq ztl(HBNz)XShY8TOn+dc0$?(KW!`Y(otdv~DJJ)4FiOeYG#Wp|aST08DwfGjK<}4Ay zVP^2K#I{i7L+L6c0=*M=gN|oNln96#%39{xiuOA>vc(sPwp!kigt!pX=>vay$bKo~dE9Q+?EN!U-gOv-C43}Uk@7-P&GxO~w-Gf$XUu0VzR-87vSJD^hdUaqX zkO|z>n}5v2*FEEm@h7=8(4AN7A!gP9=eFP;112CTgMgDp($%^OesWxd1RYTB}7U(gS90 zEZvv9r1DXPd-`BdCpox`4}G-c4GGSN=#o-MLII8M`N1bj7#RS#6WC_R?7s5+d=(2y zkAuT?#PB#7yx?}Y{a7u3XUAuv!G<7ozc^?Wdy6j{g*Ss#wp9HfEP5G%a@v1skXRiW&F-J2g;oeFe>*thO0+~B3qu|t#A;Owge zxY9N^_luOGFR-hIbSeE07GwjO+B%6aaqZ9}b)fp!<47`RrkPvGaO4!{#Ruz zDB<@{UgH}@!r(hNj38@~3E6F8MBKmV7gotO|GEHMQ{&;BZhe@e1vRkyxQ{Dw$J@`+ z)V$Hd?+ETV&WI5`Gh8q`LX5euBhl}j^<39X(omrOtL5A7!)DH79;3=hRjU%snv?N* zlgKl5+tl)0?(iC)u(lTJ3#$tt=1UEEDHA3ejMHruSyhAckl*fhqh$rKTBPp#wweMi zbp!CZ=b=^KMJz%(o1KvOYGDNMPd0n{efLkT9hE)MN;d6*#D)3SGZbu)9zPr}^;rkH zy1(dN;1^qMgK(#zXgWO2jXx0_DTS!)i)@vBhk}Lug#;fc+eGypyxmPR5)9Q~x4WfJ zQp~(Q#k2w8{Z+-PbC21X{36CO@vcZ6hbdcq;zxPH?BaXEmSOfX1bA>q{%}Nd& zl@=>4VRALcnPiwBa-)M*C*+y$9wpz&wU~kW;;L>{d6?HhWO4XBKjh+s*nm{Z+03Nc z;@h0qrdk5@OUv+P+#ylo<)lY*pId~YjG~RQ)K;DtfAP9vdyGWeJuC<`HEETZuF~Ot z<-h>(i+SN#h1Zto*Nx=j`53_R+SvD}Y31iTNZvYKpj@z1pN_(O3l$|Al@@l#v)p=1 zv*>|$j2B)#eN_a^)#mrAvI;062cFlPqV9zi-&%^iSB~cRzpU-HDu66;!+o7JuT0ig zad%_X%Q_{cTLPhB{BRm*m1Utj)C<7!#>7vwD*dy@y?M+eiE0Yhq6(C>lTq;22P+`k zFcv|6=@o?I?`Ll-YBn5xP>zG{Ei3^)>gOG(DivgbfLwF<3|U-PliOdon}L ziK$*E^2EKy#PXZaM8qoWc~^p4N?_EJE>t;Ez0_wA8b?5MW(C@&K! z@@m_yvHy~~yEuW%@?b7xcb*GwFZ^QYCvJ1kYxo9)S5!Qig;tImj5f-Q9?S+cPl0Io z8(qK63tFiH$#%!gUX4AjP6?`s9mfH?Tq}LcQ@Ok_+8C^SQd6n{6S}on0u&8!tT9df z0*HSWFDpPTZ2PuJxL75O0U)Q)z5M~}mF0MC;R>lh%uf6qy6|gLMLuBU;}%yd^hoDB zf>uCDSyv>Lc{LuD_$j?|;uEuY=IJM+Ht^GXS-@~+v*({Oeu@Pw^*}VGR3fN z;{};J8|W!YWK{W~sskgTf-)3d)a<5CixUA`3n?(%0-&jG4)`Jq4W`>j9iHI=(Y0Ul zdE`_wpj0`vV_cFg@u@x_s_tMWXotFgq*45&Ah8ko6yD|iwaM#N#6TT(&SEludx5I4 zuL*N9BD9Ymc0CFe${?J0cJCb5J359=g@tOYZjVJO-I4SBXP`iA6gLomK5VAM;I&p? z>n{vqa#!<&{_6Mn_4+!==Sqk{{myX76->mxu97F*e4y~qh_;Kl9_BpUup0EPk3xp1 zmm)?mIGc3JO5&-tmy=n@aa<(N<&8~c6~YrrA#)y_8FplrjmB`&{PpCKm9xssL+19> zxIKhA9;CCxI@bb1Lm{yMWj1ta`pL`}qGg+^IrV2g)fj^2axw9S7^~#8piUrSc9@%| zKezBatj~7};-XVEO@5XD9wDKO#u(0NzJ5`Bwqta$7lh^H@^=`X)VGjjHH!e?L=*!^ zyD>|zC5cWW#aEg$8{+)@UrpWO>O^(~Zh8x}oiS*)M5G&f|fJW84QxZrxmlnh0}mWc1#SD@Vx zW9q<=%oCjl78$RZkF0*-&q|MYXdu>WB^(+RiOp(>KWOHa$L-s{9+2Iow`030Weqm+ z^t!KnLt~{wP_4BiEY-^=isVntkoW>EdtDwRDN2!YR4q!yY`5+V{8dPQ}BxcNH+4MW``^me{07`CFQbP}hy8`A~%ADokL+~sNSg9+;`ZdHg4A#PxCmX zv8tN*ZsJbD-3|l+Q?xv0FJKSq4z^G=L+5wR&{=nKTW!EqRs!i4-YK z6Q_po4u5^6kQj_XcB^E9bIaC+PYh4UX)bWv$QjX>;auTxr{P826oQnaIF1r=RaArJ z2DUHxrPQOn{Yhg(P;wt%-iZ&^`PWmovN!g;p8O0OIZyrBW%5G1{Qco2tk(zkmzDUIFE+zTPlXTx`F+Cn)lYeo z%@Puz_3YO508LyDw!Z|{gEk&}zyuSmtkXXz=JI=D3^BMgwbo9rkwg4XhR)rYd zUuU%6aDy&YC|Wd!1^=kU%=7G}%l`SKwLMzr7; zTFqF}U&j1zHp%w+u)zOc&uHOhRH_Gm3l5S_@pyL^gA!`|y>X}1KD=I^NO@E6N2CAG z(3)M`~WM2P9a-ofjL4R0UaKmU`R?z+Wo;Np0MQI@A!sgu3?$+HA8l^(?q+Vp@jUsAr=y~rh0uM z06KXO3!+J@ND8ueV%GQDi~Wjds*;vrN1zxv;Oy*}MGO*B=HuQzbK@H*n`svr zGkuQEOYMa$0xH|r23J9Au-}g}6V~5Eoj>a@JD!bsT$lhQuixzO+uIW*t$xs0#j8Js z>R4xi8VGR~UyHSs&8vJozZ)B8c zR@58rt&-!@!~3(|oRxkxWixTXo|MgVYH|%cFPc)Ucd8wEmvalN?LAxM*k;uJSi7*> zP8YQq{Eq>)eBrF$b>f3iTmC=x-YP1tu3Og)7D8|mEV#RS@Zj$5?(Uw1;O-8AV1>K8 zyBDs(Dcq{?UH@9&`q$d+>~nQ4&u(qrR;xM3m}AV*dw<@y%W9P7y-zBsW;}U6&5gR4 z4TME!A?caMHC$|6NG9dKNN?Jcci|RljOEK1E7d%(3bH0BO|+o8=qz_YytTio%VZDL z`Y!G5W9#%3g8skGJgZ+!qI-! zXZh;t_|eR(x@rlqF?koV*~#{{qQ1V?e#Ox*!)(=DHa;nx$NlYkIdYE*!NW%bhm_9& z83QBcaQMDj?^Q34pX8*X5!AD9RuPobwD?kPfwuhURwz-ml}fkBH!-*&ggZrs?e*Wg z74y4)O?L);{#BK?GS|S8?Gf%<&YQc9G%l=T3b&Fi@$jCdNuFnQB;brX^b^Q zTQ#PJRcdU>P!4vp(tp`!-Fe7>oeX0JTi*s{<_jYG?B%)=5^AM&G>ku{6Ah=8YmqlY ze$!dRObVeOAuDT}pv`dpwVN^lf06I-OZ)&+p6i2GTVH?Yn4HoUl)Mmg9hZ|DTOfAO zp+KP5iHD9YGef?pZls#FvLcma;wmoUqm#b@n`^4a_Rb3xZRPjZ^lLU%yGkSK z6O1hT02^Ty3-IZn`S|$ApRP-pF9^Q`f3_%Zizu>do@uV()gFYH^I1K!a_vtat!lRx z+}`x~w=E=Br-1^wWeHC=@AV`5l64r2Z5p&RLNx&M^27P(1W2k%zJ;wSaBeV+)m(~G zqBM8p!xxvE8r@TSRgE^s_HsTYOwv+i*(?Cvav?q?qMXB2_SGpudLP_+`NOP-Hpba6 zQ(S^9ylul}IBaSt;W>?CuG5t(oy2Md1@Uc~=d;|yM?HO09=fUyP-nEFj;T|715z!| z-6j9=t)%fqmSi_)n1=B_^}^MFlV|Q_C!3Q!U6tXP?XLOl&G)=d{x!{=yHDAlmwWFD z4dm{Ha0zob?#jjdE>0ioHhNO*mS$blKQpF$P1gYPIa_CW8X2{0`U~@z)~^o_PZ-*{ zz44@pjC+}G9Lk?WGNrng1=5VY&`@Xy(+k@ZI=a+FUPM2pyKZU-joKPOR# zLh8EnWILJ#(2uY~^d8rLviig`8@A~px1`UP8|ye1!sOR+x|tbZRJ2(OUhk8wVc^oy zoeCgX<1ZK+9mCj9PfGY)e2~vH@8JRtYNraI=}R}Eqe#nrad8{bxMV*U8+#cGLaf51 z5Hk--c9<6ZE9SeYsRybb+VDQl7vHlN{uQ1sla8^1q=Sz?!oW1R^QlDn$&Al4S!U&L zQi9$=*bI!|<$R_(CE&4J_F<5$!-}iPlG@B7;oR%_$#joVR`dWY8lFWDYS$#CabRC0 zaflm-t^S7CcxXm6bFm1T0T={h?MT~XEGXf)Mt-%6oef?!mRr;xC$1CvWSdDXhPoSjj64$w^)`9%OvA;pO~SJoe58rfOxzSW)j3(NBp1{GM^sK}lRB|K z72XC4&Rkyx1mbpJo(Nc$*j@x!4?Q231Cxo@Vs`IMvs|-N-9}k;+7QtOVk;o?v=EuzP>7;Z$}e7hRz-SmAh^;I-R_FR%%wR&8r>8ga?)m zqG`^-nw$Ntd?O51A}b~@Up&2Y^@PtdclOBq#tI*neM`eU6_vs}B-@z}yEF=mtW^!C z&TEMUSx;ljm76^94M#N5Z(W6B^TLRR-;+O930-%cceq>EcJFxjM(CF;f>{pt9oDBN zOamQGGo51muBkQxTq?+M2|SNrR5Vhnd8(3c+L5c;^>t5mm16Y!>1~3a6g!8?VgiIX zmtXy?zr);`(Ya9TO{fNtx9zRVr@AG_w1zNr|=i z2`))&WCb*lH|+6`sjvLO^{zT+q4od%koPdb3#=C;GbxXdbZ=6%LQEEd%~;uI2q-Sv zFp=~0p@B8rJ}8YsYf&+9A2qr|H9;VJ%}+t>c`jucpCA4O^b%{-%{z4MFp`|gkpM7J z)2*qVz#W}PsKt$lg1aQyuEKutQk-Y0(%^8tQvyDpp% zjveP=L&j|8bZO1_<>=9I-9hrRcf>Dwm;~)sUc(cK;`l1aUOwM)P!9>4%=}K{u44rO zH~#$4y_y|?ys!V*>J>BmL>GY#pAId)@H;=FvG{o>cuh;i*_*g70y$K43Q&e6qgUkto54vWTTv4y(j85dhWy1lTG<0f17HpI`9d#3 z56>!oJ*yl!pWJ5;Lmb|+q~n0QgJ9L#Bc=dER0eP)^d zpUi`&&9*MG2*31OL69&~A6?#|qCHZdDtii#q|Y18vu=a>%$@EBZyvaQ8y-NHGB>)f z9(<S$ckRclK2xuBTj0RN9c= z1)EQos)T=s?_By!&TZbJFN+wTra{)6Dm5#$(Op?QRJHTDwR@@iLfe74o@;q;?`Fqa z9qhYCQ0qBH8wPG=vw0yjR1$ueu6t^oNuIW_T1q=}0e030oe9m1y!f*A)u0gYT!{Kh z7A4UNu zl0}mtcuk_1fP89vqvRo_A55dcDGM6<-@9{PMEu5rWREiScfay&e4YgGvN`nKSo2 zwUbEQ?o-WuktX&m*@%4?2bKpAc8iOz>o?Dp2oJ?vid}~xDnCuS*c?**2u|QxdSO4= zz?}^hv3XJZc@g7!>RHTz70&mZB)~7eX?3^F7*uOe2lMzR@s+iNyOQ}ye@&`qabJ4b z_gfpbdwkHAb?f4>6q_Ra|^j5@#zzoRp zLV*|)hm!Dl$_JSDCeQ{%VTXauAyMUqRtN60yHdJCSRjJVchy_!zhZRcYG0z1YCPIo zjl=~3A0w0bc|l__H1qbk%@J0sLj>q zL=8Nz=}F+%Ruf~V@$+^gzu<}Puuqaf@Ego$nRTwLf!b)WKM_r| zAqMHM>r6-Mi-{ex8N?<@7|mqp_}AtAKS{KY9 z^7eRDPP0M%Ys*Yqzgo!ysD$3GKYdOsbJ_7-i9WNO?lELwv^q4FThN( >EgVsIk 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

E7nyuHz7b7+oV6RueRc}DArB1Ndl4 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

A&J%ve3-YNg_!D99AC%SVAUW&%-C=(X!#Q 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

>G3)#xB)5=vRr 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 F{{Zr_pEm#i literal 0 HcmV?d00001 diff --git a/docs/images/aws-ec2-new-user-csv.png b/docs/images/aws-ec2-new-user-csv.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecea6746e5f238b6d133b28082772f6d2e5c42f GIT binary patch literal 77993 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%0A3~R#9F|Vy9sgKVhpC=nU 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 literal 0 HcmV?d00001 diff --git a/docs/images/aws-ec2-new-user.png b/docs/images/aws-ec2-new-user.png new file mode 100644 index 0000000000000000000000000000000000000000..86651f3eff6c93ee1349b9e4788dcfb850068379 GIT binary patch literal 112857 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 z�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,`rt;dRa2JS&19T__N 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 Date: Mon, 30 Jul 2018 01:56:01 -0400 Subject: [PATCH 649/769] Update PPA for dnscrypt-proxy to 'bionic' (#1039) --- roles/dns_encryption/tasks/ubuntu.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 5485f682..0050a58e 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -2,7 +2,7 @@ - name: Add the repository apt_repository: state: present - codename: artful + codename: bionic repo: ppa:shevchuk/dnscrypt-proxy register: result until: result|succeeded From b88f697b286d196bfb8699ce1947b1a4e883a45e Mon Sep 17 00:00:00 2001 From: Quentin Moss Date: Mon, 30 Jul 2018 06:01:03 -0700 Subject: [PATCH 650/769] Update troubleshooting docs to include iOS reconnection loop (#1042) * Update troubleshooting docs to include iOS reconnection loop * nits --- docs/troubleshooting.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c16ed9fb..26084eb5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,6 +18,7 @@ * [I can't get my router to connect to the Algo server](#i-cant-get-my-router-to-connect-to-the-algo-server) * [I can't get Network Manager to connect to the Algo server](#i-cant-get-network-manager-to-connect-to-the-algo-server) * [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) + * [Devices appear to be stuck in reconnection loop](#devices-appear-to-be-stuck-in-reconnection-loop) * ["Error 809" or IKE_AUTH requests that never make it to the server](#error-809-or-ike_auth-requests-that-never-make-it-to-the-server) * [I have a problem not covered here](#i-have-a-problem-not-covered-here) @@ -213,6 +214,17 @@ $ sudo ifconfig wlan0 mtu 1438 You can also set the `max_mss` variable to a new value in config.cfg, and then redeploy your server rather than reconfigure the current one in-place. +### Clients appear stuck in a reconnection loop + +If you're using 'Connect on Demand' on iOS and your client device appears stuck in a reconnection loop after switching from WiFi to LTE or vice versa, you may want to try disabling DoS protection in strongSwan. + +The configuration value can be found in `/etc/strongswan.d/charon.conf`. After making the change you must reload or restart ipsec. + +Example command: +``` +sed -i -e 's/#*.dos_protection = yes/dos_protection = no/' /etc/strongswan.d/charon.conf && ipsec restart +``` + ### "Error 809" or IKE_AUTH requests that never make it to the server On Windows, this issue may manifest with an error message that says "The network connection between your computer and the VPN server could not be established because the remote server is not responding... This is Error 809." On other operating systems, you may try to debug the issue by capturing packets with tcpdump and notice that, while IKE_SA_INIT request and responses are exchanged between the client and server, IKE_AUTH requests never make it to the server. From 3ddd0ac30f211fcf0ba33b60e58cd158a4fe07dc Mon Sep 17 00:00:00 2001 From: Fabian Foerg Date: Mon, 30 Jul 2018 06:01:49 -0700 Subject: [PATCH 651/769] Run dnsmasq as the dnsmasq user (#1029) * Run dnsmasq as the dnsmasq user There is a task that checks whether the dnsmasq user exists. However, dnsmasq is configured to run as user "nobody" instead. This change lets dnsmasq run as user "dnsmasq". * remove dnsmasq user task --- roles/dns_adblocking/tasks/main.yml | 3 --- roles/dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index ded3f798..a68abeed 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -8,9 +8,6 @@ - name: Dnsmasq installed package: name=dnsmasq - - name: Ensure that the dnsmasq user exist - user: name=dnsmasq groups=nogroup append=yes state=present - - name: The dnsmasq directory created file: dest=/var/lib/dnsmasq state=directory mode=0755 owner=dnsmasq group=nogroup diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 501f7568..135aeb18 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -103,7 +103,7 @@ server={{ host }} # If you want dnsmasq to change uid and gid to something other # than the default, edit the following lines. -user=nobody +user=dnsmasq group=nogroup # If you want dnsmasq to listen for DHCP and DNS requests only on From e0c317a9588af7b9bf6e846b669b3b3eeb2e034b Mon Sep 17 00:00:00 2001 From: Quentin Moss Date: Mon, 30 Jul 2018 07:28:14 -0700 Subject: [PATCH 652/769] Update documentation link (#1043) --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 26084eb5..632696d9 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,7 +18,7 @@ * [I can't get my router to connect to the Algo server](#i-cant-get-my-router-to-connect-to-the-algo-server) * [I can't get Network Manager to connect to the Algo server](#i-cant-get-network-manager-to-connect-to-the-algo-server) * [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) - * [Devices appear to be stuck in reconnection loop](#devices-appear-to-be-stuck-in-reconnection-loop) + * [Clients appear stuck in a reconnection loop](#clients-appear-stuck-in-a-reconnection-loop) * ["Error 809" or IKE_AUTH requests that never make it to the server](#error-809-or-ike_auth-requests-that-never-make-it-to-the-server) * [I have a problem not covered here](#i-have-a-problem-not-covered-here) From b86ebe20d7900a9a8898e5173a42ccb4dc7ee422 Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 8 Aug 2018 00:25:33 -0400 Subject: [PATCH 653/769] Prevent DNS rebinding (#1049) --- .../dns_adblocking/templates/dnsmasq.conf.j2 | 1 + roles/dns_encryption/tasks/main.yml | 7 +++ .../templates/dnscrypt-proxy.toml.j2 | 2 +- .../templates/ip-blacklist.txt.j2 | 44 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 roles/dns_encryption/templates/ip-blacklist.txt.j2 diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 135aeb18..0e6e72f5 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -94,6 +94,7 @@ server={{ local_service_ip }}#5353 {% for host in dns_servers.ipv4 %} server={{ host }} {% endfor %} +stop-dns-rebind {% endif %} # and this sets the source (ie local) address used to talk to diff --git a/roles/dns_encryption/tasks/main.yml b/roles/dns_encryption/tasks/main.yml index 49c8d6e8..5740703c 100644 --- a/roles/dns_encryption/tasks/main.yml +++ b/roles/dns_encryption/tasks/main.yml @@ -7,6 +7,13 @@ include_tasks: freebsd.yml when: ansible_distribution == 'FreeBSD' +- name: dnscrypt-proxy ip-blacklist configured + template: + src: ip-blacklist.txt.j2 + dest: "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/ip-blacklist.txt" + notify: + - restart dnscrypt-proxy + - name: dnscrypt-proxy configured template: src: dnscrypt-proxy.toml.j2 diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 22e9cfc5..f99aeda0 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -343,7 +343,7 @@ cache_neg_max_ttl = 600 ## Path to the file of blocking rules (absolute, or relative to the same directory as the executable file) - # blacklist_file = 'ip-blacklist.txt' + blacklist_file = 'ip-blacklist.txt' ## Optional path to a file logging blocked queries diff --git a/roles/dns_encryption/templates/ip-blacklist.txt.j2 b/roles/dns_encryption/templates/ip-blacklist.txt.j2 new file mode 100644 index 00000000..d2189ff2 --- /dev/null +++ b/roles/dns_encryption/templates/ip-blacklist.txt.j2 @@ -0,0 +1,44 @@ +0.0.0.0 +10.* +127.* +169.254.* +172.16.* +172.17.* +172.18.* +172.19.* +172.20.* +172.21.* +172.22.* +172.23.* +172.24.* +172.25.* +172.26.* +172.27.* +172.28.* +172.29.* +172.30.* +172.31.* +192.168.* +::ffff:0.0.0.0 +::ffff:10.* +::ffff:127.* +::ffff:169.254.* +::ffff:172.16.* +::ffff:172.17.* +::ffff:172.18.* +::ffff:172.19.* +::ffff:172.20.* +::ffff:172.21.* +::ffff:172.22.* +::ffff:172.23.* +::ffff:172.24.* +::ffff:172.25.* +::ffff:172.26.* +::ffff:172.27.* +::ffff:172.28.* +::ffff:172.29.* +::ffff:172.30.* +::ffff:172.31.* +::ffff:192.168.* +fd00::* +fe80::* From 53d1113881e6b951cb5162ba987c5f583d918f9b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 8 Aug 2018 07:25:59 +0300 Subject: [PATCH 654/769] Split up unattended upgrades (#1041) --- roles/common/templates/50unattended-upgrades.j2 | 3 --- .../files/50-dnscrypt-proxy-unattended-upgrades | 4 ++++ roles/dns_encryption/tasks/ubuntu.yml | 10 +++++++++- roles/wireguard/files/50-wireguard-unattended-upgrades | 4 ++++ roles/wireguard/tasks/main.yml | 8 ++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades create mode 100644 roles/wireguard/files/50-wireguard-unattended-upgrades diff --git a/roles/common/templates/50unattended-upgrades.j2 b/roles/common/templates/50unattended-upgrades.j2 index a902c7ad..0c55b702 100644 --- a/roles/common/templates/50unattended-upgrades.j2 +++ b/roles/common/templates/50unattended-upgrades.j2 @@ -2,9 +2,6 @@ Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; "${distro_id}:${distro_codename}-updates"; -{% if wireguard_enabled %} - "LP-PPA-wireguard-wireguard:${distro_codename}"; -{% endif %} // "${distro_id}:${distro_codename}-proposed"; // "${distro_id}:${distro_codename}-backports"; }; diff --git a/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades b/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades new file mode 100644 index 00000000..632bb318 --- /dev/null +++ b/roles/dns_encryption/files/50-dnscrypt-proxy-unattended-upgrades @@ -0,0 +1,4 @@ +// Automatically upgrade packages from these (origin:archive) pairs +Unattended-Upgrade::Allowed-Origins { + "LP-PPA-shevchuk-dnscrypt-proxy:${distro_codename}"; +}; diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 0050a58e..f42d0a90 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -8,13 +8,21 @@ until: result|succeeded retries: 10 delay: 3 - + - name: Install dnscrypt-proxy apt: name: dnscrypt-proxy state: latest update_cache: true +- name: Configure unattended-upgrades + copy: + src: 50-dnscrypt-proxy-unattended-upgrades + dest: /etc/apt/apt.conf.d/50-dnscrypt-proxy-unattended-upgrades + owner: root + group: root + mode: 0644 + - block: - name: Ubuntu | Unbound profile for apparmor configured copy: diff --git a/roles/wireguard/files/50-wireguard-unattended-upgrades b/roles/wireguard/files/50-wireguard-unattended-upgrades new file mode 100644 index 00000000..b1ffc97d --- /dev/null +++ b/roles/wireguard/files/50-wireguard-unattended-upgrades @@ -0,0 +1,4 @@ +// Automatically upgrade packages from these (origin:archive) pairs +Unattended-Upgrade::Allowed-Origins { + "LP-PPA-wireguard-wireguard:${distro_codename}"; +}; diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 4b70a3a2..df5b832e 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -14,6 +14,14 @@ state: present update_cache: true +- name: Configure unattended-upgrades + copy: + src: 50-wireguard-unattended-upgrades + dest: /etc/apt/apt.conf.d/50-wireguard-unattended-upgrades + owner: root + group: root + mode: 0644 + - name: Ensure the required directories exist file: dest: "{{ wireguard_config_path }}/{{ item }}" From a57a0adf5e1d0aeeea366c4c6ba4c7c5c60f3a45 Mon Sep 17 00:00:00 2001 From: Josh Dimarsky <24758845+yehoshuadimarsky@users.noreply.github.com> Date: Fri, 24 Aug 2018 04:42:59 -0400 Subject: [PATCH 655/769] Fixed broken link; clarified example docker command (#1064) --- docs/Docker.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Docker.md b/docs/Docker.md index fba31193..65f363b9 100644 --- a/docs/Docker.md +++ b/docs/Docker.md @@ -4,7 +4,7 @@ While it is not possible to run your Algo server from within a Docker container, ## Limitations -1. [Advanced](ADVANCED.md) installations are not currently supported; you must use the interactive `algo` script. +1. [Advanced](deploy-from-ansible.md) installations are not currently supported; you must use the interactive `algo` script. 2. This has not yet been tested with user namespacing enabled. 3. If you're running this on Windows, take care when editing files under `configs/` to ensure that line endings are set appropriately for Unix systems. @@ -13,7 +13,7 @@ While it is not possible to run your Algo server from within a Docker container, 1. Install [Docker](https://www.docker.com/community-edition#/download) -- setup and configuration is not covered here 2. Create a local directory to hold your VPN configs (e.g. `C:\Users\trailofbits\Documents\VPNs\`) 3. Create a local copy of [config.cfg](https://github.com/trailofbits/algo/blob/master/config.cfg), with required modifications (e.g. `C:\Users\trailofbits\Documents\VPNs\config.cfg`) -4. Run the Docker container, mounting your configurations appropriately: +4. Run the Docker container, mounting your configurations appropriately (assuming the container is named `trailofbits/algo` with a tag `latest`): - From Windows: ```powershell C:\Users\trailofbits> docker run --cap-drop=all -it \ @@ -61,7 +61,7 @@ Docker themselves provide a concept of [Content Trust](https://docs.docker.com/e ## Future Improvements 1. Even though we're taking care to drop all capabilities to minimize the impact of running as root, we can probably include not only a `seccomp` profile, but also AppArmor and/or SELinux profiles as well. -2. The Docker image doesn't natively support [advanced](ADVANCED.md) Algo deployments, which is useful for scripting. This can be done by launching an interactive shell and running the commands yourself. +2. The Docker image doesn't natively support [advanced](deploy-from-ansible.md) Algo deployments, which is useful for scripting. This can be done by launching an interactive shell and running the commands yourself. 3. The way configuration is passed into and out of the container is a bit kludgy. Hopefully future improvements in Docker volumes will make this a bit easier to handle. ## Advanced Usage From e8947f318b197cc8e7c3dfeb7a1289f2593f3b6c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 17:05:45 +0300 Subject: [PATCH 656/769] Large refactor to support Ansible 2.5 (#976) * Refactoring, booleans declaration and update users fix * Make server_name more FQDN compatible * Rename variables * Define the default value for store_cakey * Skip a prompt about the SSH user if deploying to localhost * Disable reboot for non-cloud deployments * Enable EC2 volume encryption by default * Add default server value (localhost) for the local installation Delete empty files * Add default region to aws_region_facts * Update docs * EC2 credentials fix * Warnings fix * Update deploy-from-ansible.md * Fix a typo * Remove lightsail from the docs * Disable EC2 encryption by default * rename droplet to server * Disable dependencies * Disable tls_cipher_suite * Convert wifi-exclude to a string. Update-users fix * SSH access congrats fix * 16.04 > 18.04 * Dont ask for the credentials if specified in the environment vars * GCE server name fix --- .travis.yml | 5 +- algo | 641 +----------------- ansible.cfg | 1 + cloud.yml | 49 ++ config.cfg | 21 +- deploy.yml | 98 --- docs/cloud-do.md | 6 +- docs/cloud-vultr.md | 8 + docs/deploy-from-ansible.md | 223 +++--- docs/deploy-to-freebsd.md | 4 +- docs/index.md | 1 + input.yml | 137 ++++ library/digital_ocean_tag.py | 217 ------ library/ec2_ami_copy.py | 216 ------ library/gce_region_facts.py | 139 ++++ library/lightsail_region_facts.py | 102 +++ main.yml | 9 + playbooks/cloud-post.yml | 45 ++ playbooks/cloud-pre.yml | 13 + playbooks/common.yml | 15 - playbooks/facts/FreeBSD.yml | 10 - playbooks/facts/main.yml | 44 -- playbooks/freebsd.yml | 9 - playbooks/local.yml | 31 - playbooks/local_ssh.yml | 12 - playbooks/post.yml | 16 - playbooks/ubuntu.yml | 14 - requirements.txt | 2 +- roles/client/tasks/main.yml | 2 +- roles/cloud-azure/defaults/main.yml | 214 ++++++ roles/cloud-azure/handlers/main.yml | 0 roles/cloud-azure/tasks/main.yml | 26 +- roles/cloud-azure/tasks/prompts.yml | 70 ++ roles/cloud-digitalocean/handlers/main.yml | 0 roles/cloud-digitalocean/tasks/main.yml | 66 +- roles/cloud-digitalocean/tasks/prompts.yml | 46 ++ .../templates/20-ipv6.cfg.j2 | 6 - roles/cloud-ec2/defaults/main.yml | 3 +- roles/cloud-ec2/handlers/main.yml | 0 roles/cloud-ec2/tasks/cloudformation.yml | 6 +- roles/cloud-ec2/tasks/encrypt_image.yml | 44 +- roles/cloud-ec2/tasks/main.yml | 74 +- roles/cloud-ec2/tasks/prompts.yml | 55 ++ roles/cloud-gce/handlers/main.yml | 0 roles/cloud-gce/tasks/main.yml | 53 +- roles/cloud-gce/tasks/prompts.yml | 67 ++ roles/cloud-lightsail/tasks/main.yml | 18 +- roles/cloud-lightsail/tasks/prompts.yml | 60 ++ roles/cloud-openstack/tasks/main.yml | 12 +- roles/cloud-scaleway/defaults/main.yml | 4 + roles/cloud-scaleway/tasks/main.yml | 25 +- roles/cloud-scaleway/tasks/prompts.yml | 34 + roles/cloud-vultr/tasks/main.yml | 36 + roles/cloud-vultr/tasks/prompts.yml | 56 ++ roles/common/tasks/facts.yml | 26 + roles/common/tasks/freebsd.yml | 18 +- roles/common/tasks/main.yml | 34 +- roles/common/tasks/ubuntu.yml | 106 +-- roles/dns_adblocking/meta/main.yml | 7 - roles/dns_adblocking/tasks/main.yml | 5 - .../dns_adblocking/templates/dnsmasq.conf.j2 | 2 +- roles/dns_encryption/defaults/main.yml | 4 +- roles/dns_encryption/handlers/main.yml | 7 + roles/dns_encryption/meta/main.yml | 4 - roles/dns_encryption/tasks/ubuntu.yml | 2 +- .../templates/dnscrypt-proxy.toml.j2 | 2 +- roles/local/handlers/main.yml | 0 roles/local/tasks/main.yml | 36 +- roles/local/tasks/prompts.yml | 44 ++ roles/ssh_tunneling/meta/main.yml | 4 - roles/ssh_tunneling/tasks/main.yml | 22 +- roles/vpn/defaults/main.yml | 32 + roles/vpn/meta/main.yml | 3 +- roles/vpn/tasks/client_configs.yml | 13 +- roles/vpn/tasks/freebsd.yml | 114 ---- roles/vpn/tasks/main.yml | 26 +- roles/vpn/tasks/openssl.yml | 18 +- roles/vpn/templates/client_ipsec.conf.j2 | 2 +- roles/vpn/templates/ipsec.conf.j2 | 4 +- roles/vpn/templates/mobileconfig.j2 | 10 +- roles/vpn/templates/rules.v4.j2 | 2 +- roles/vpn/templates/rules.v6.j2 | 2 +- roles/wireguard/defaults/main.yml | 24 - roles/wireguard/meta/main.yml | 3 - roles/wireguard/tasks/keys.yml | 3 +- roles/wireguard/tasks/main.yml | 2 +- server.yml | 65 ++ tests/local-deploy.sh | 7 +- tests/update-users.sh | 11 +- users.yml | 76 ++- 90 files changed, 1774 insertions(+), 2031 deletions(-) create mode 100644 cloud.yml delete mode 100644 deploy.yml create mode 100644 docs/cloud-vultr.md create mode 100644 input.yml delete mode 100644 library/digital_ocean_tag.py delete mode 100644 library/ec2_ami_copy.py create mode 100644 library/gce_region_facts.py create mode 100644 library/lightsail_region_facts.py create mode 100644 main.yml create mode 100644 playbooks/cloud-post.yml create mode 100644 playbooks/cloud-pre.yml delete mode 100644 playbooks/common.yml delete mode 100644 playbooks/facts/FreeBSD.yml delete mode 100644 playbooks/facts/main.yml delete mode 100644 playbooks/freebsd.yml delete mode 100644 playbooks/local.yml delete mode 100644 playbooks/local_ssh.yml delete mode 100644 playbooks/post.yml delete mode 100644 playbooks/ubuntu.yml create mode 100644 roles/cloud-azure/defaults/main.yml delete mode 100644 roles/cloud-azure/handlers/main.yml create mode 100644 roles/cloud-azure/tasks/prompts.yml delete mode 100644 roles/cloud-digitalocean/handlers/main.yml create mode 100644 roles/cloud-digitalocean/tasks/prompts.yml delete mode 100644 roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 delete mode 100644 roles/cloud-ec2/handlers/main.yml create mode 100644 roles/cloud-ec2/tasks/prompts.yml delete mode 100644 roles/cloud-gce/handlers/main.yml create mode 100644 roles/cloud-gce/tasks/prompts.yml create mode 100644 roles/cloud-lightsail/tasks/prompts.yml create mode 100644 roles/cloud-scaleway/defaults/main.yml create mode 100644 roles/cloud-scaleway/tasks/prompts.yml create mode 100644 roles/cloud-vultr/tasks/main.yml create mode 100644 roles/cloud-vultr/tasks/prompts.yml create mode 100644 roles/common/tasks/facts.yml delete mode 100644 roles/dns_adblocking/meta/main.yml delete mode 100644 roles/dns_encryption/meta/main.yml delete mode 100644 roles/local/handlers/main.yml create mode 100644 roles/local/tasks/prompts.yml delete mode 100644 roles/ssh_tunneling/meta/main.yml delete mode 100644 roles/vpn/tasks/freebsd.yml delete mode 100644 roles/wireguard/defaults/main.yml delete mode 100644 roles/wireguard/meta/main.yml create mode 100644 server.yml diff --git a/.travis.yml b/.travis.yml index 9d91089e..47a58a95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,6 @@ before_cache: - sudo chown $USER. $HOME/lxc/cache.tar env: - - LXC_NAME=ubuntu1804 LXC_DISTRO=ubuntu LXC_RELEASE=18.04 - LXC_NAME=docker LXC_DISTRO=ubuntu LXC_RELEASE=18.04 before_install: @@ -67,8 +66,8 @@ install: script: # - awesome_bot --allow-dupe --skip-save-results *.md docs/*.md --white-list paypal.com,do.co,microsoft.com,https://github.com/trailofbits/algo/archive/master.zip,https://github.com/trailofbits/algo/issues/new # - shellcheck algo -# - ansible-lint deploy.yml users.yml deploy_client.yml - - ansible-playbook deploy.yml --syntax-check +# - ansible-lint main.yml users.yml deploy_client.yml + - ansible-playbook main.yml --syntax-check - ./tests/local-deploy.sh - ./tests/update-users.sh diff --git a/algo b/algo index 3c17a7d8..07a2875c 100755 --- a/algo +++ b/algo @@ -14,642 +14,9 @@ then fi fi -SKIP_TAGS="_null encrypted" -ADDITIONAL_PROMPT="[pasted values will not be displayed]" - -additional_roles () { - -read -p " -Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to cellular networks? -[y/N]: " -r OnDemandEnabled_Cellular -OnDemandEnabled_Cellular=${OnDemandEnabled_Cellular:-n} -if [[ "$OnDemandEnabled_Cellular" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_Cellular=Y"; fi - -read -p " -Do you want macOS/iOS clients to enable \"VPN On Demand\" when connected to Wi-Fi? -[y/N]: " -r OnDemandEnabled_WIFI -OnDemandEnabled_WIFI=${OnDemandEnabled_WIFI:-n} -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" OnDemandEnabled_WIFI=Y"; fi - -if [[ "$OnDemandEnabled_WIFI" =~ ^(y|Y)$ ]]; then - read -p " -List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) -: " -r OnDemandEnabled_WIFI_EXCLUDE - OnDemandEnabled_WIFI_EXCLUDE=${OnDemandEnabled_WIFI_EXCLUDE:-_null} - EXTRA_VARS+=" OnDemandEnabled_WIFI_EXCLUDE=\"$OnDemandEnabled_WIFI_EXCLUDE\"" -fi - -read -p " -Do you want to install a DNS resolver on this VPN server, to block ads while surfing? -[y/N]: " -r dns_enabled -dns_enabled=${dns_enabled:-n} -if [[ "$dns_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" dns"; EXTRA_VARS+=" local_dns=true"; fi - -read -p " -Do you want each user to have their own account for SSH tunneling? -[y/N]: " -r ssh_tunneling_enabled -ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} -if [[ "$ssh_tunneling_enabled" =~ ^(y|Y)$ ]]; then ROLES+=" ssh_tunneling"; fi - -read -p " -Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) -[y/N]: " -r Win10_Enabled -Win10_Enabled=${Win10_Enabled:-n} -if [[ "$Win10_Enabled" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Win10_Enabled=Y"; fi - -read -p " -Do you want to retain the CA key? (required to add users in the future, but less secure) -[y/N]: " -r Store_CAKEY -Store_CAKEY=${Store_CAKEY:-N} -if [[ "$Store_CAKEY" =~ ^(n|N)$ ]]; then EXTRA_VARS+=" Store_CAKEY=N"; fi - -} - -deploy () { - - ansible-playbook deploy.yml -t "${ROLES// /,}" -e "${EXTRA_VARS}" --skip-tags "${SKIP_TAGS// /,}" - -} - -azure () { - read -p " -Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_secret - - read -p " - -Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_tenant - - read -p " - -Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_client_id - - read -p " - -Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) -You can skip this step if you want to use your defaults credentials from ~/.azure/credentials -$ADDITIONAL_PROMPT -[...]: " -rs azure_subscription_id - - read -p " - -Name the vpn server: -[algo]: " -r azure_server_name - azure_server_name=${azure_server_name:-algo} - - read -p " - - What region should the server be located in? (https://azure.microsoft.com/en-us/regions/) - 1. East US (Virginia) - 2. East US 2 (Virginia) - 3. Central US (Iowa) - 4. North Central US (Illinois) - 5. South Central US (Texas) - 6. West Central US (Wyoming) - 7. West US (California) - 8. West US 2 (Washington) - 9. Canada East (Quebec City) - 10. Canada Central (Toronto) - 11. Brazil South (Sao Paulo State) - 12. North Europe (Ireland) - 13. West Europe (Netherlands) - 14. France Central (Paris) - 15. France South (Marseille) - 16. UK West (Cardiff) - 17. UK South (London) - 18. Germany Central (Frankfurt) - 19. Germany Northeast (Magdeburg) - 20. Southeast Asia (Singapore) - 21. East Asia (Hong Kong) - 22. Australia East (New South Wales) - 23. Australia Southeast (Victoria) - 24. Australia Central (Canberra) - 25. Australia Central 2 (Canberra) - 26. Central India (Pune) - 27. West India (Mumbai) - 28. South India (Chennai) - 29. Japan East (Tokyo, Saitama) - 30. Japan West (Osaka) - 31. Korea Central (Seoul) - 32. Korea South (Busan) - -Enter the number of your desired region: -[1]: " -r azure_region - azure_region=${azure_region:-1} - - case "$azure_region" in - 1) region="eastus" ;; - 2) region="eastus2" ;; - 3) region="centralus" ;; - 4) region="northcentralus" ;; - 5) region="southcentralus" ;; - 6) region="westcentralus" ;; - 7) region="westus" ;; - 8) region="westus2" ;; - 9) region="canadaeast" ;; - 10) region="canadacentral" ;; - 11) region="brazilsouth" ;; - 12) region="northeurope" ;; - 13) region="westeurope" ;; - 14) region="francecentral" ;; - 15) region="francesouth" ;; - 16) region="ukwest" ;; - 17) region="uksouth" ;; - 18) region="germanycentral" ;; - 19) region="germanynortheast" ;; - 20) region="southeastasia" ;; - 21) region="eastasia" ;; - 22) region="australiaeast" ;; - 23) region="australiasoutheast" ;; - 24) region="australiacentral" ;; - 25) region="australiacentral2" ;; - 26) region="centralindia" ;; - 27) region="westindia" ;; - 28) region="southindia" ;; - 29) region="japaneast" ;; - 30) region="japanwest" ;; - 31) region="koreacentral" ;; - 32) region="koreasouth" ;; - esac - - ROLES="azure vpn cloud" - EXTRA_VARS="azure_secret=$azure_secret azure_tenant=$azure_tenant azure_client_id=$azure_client_id azure_subscription_id=$azure_subscription_id azure_server_name=$azure_server_name ssh_public_key=$ssh_public_key region=$region" -} - -digitalocean () { - read -p " -Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): -$ADDITIONAL_PROMPT -: " -rs do_access_token - - read -p " - -Name the vpn server: -[algo.local]: " -r do_server_name - do_server_name=${do_server_name:-algo.local} - - read -p " - - What region should the server be located in? - 1. Amsterdam (Datacenter 2) - 2. Amsterdam (Datacenter 3) - 3. Frankfurt - 4. London - 5. New York (Datacenter 1) - 6. New York (Datacenter 2) - 7. New York (Datacenter 3) - 8. San Francisco (Datacenter 1) - 9. San Francisco (Datacenter 2) - 10. Singapore - 11. Toronto - 12. Bangalore - -Enter the number of your desired region: -[7]: " -r region - region=${region:-7} - - case "$region" in - 1) do_region="ams2" ;; - 2) do_region="ams3" ;; - 3) do_region="fra1" ;; - 4) do_region="lon1" ;; - 5) do_region="nyc1" ;; - 6) do_region="nyc2" ;; - 7) do_region="nyc3" ;; - 8) do_region="sfo1" ;; - 9) do_region="sfo2" ;; - 10) do_region="sgp1" ;; - 11) do_region="tor1" ;; - 12) do_region="blr1" ;; - esac - -ROLES="digitalocean vpn cloud" -EXTRA_VARS="do_access_token=$do_access_token do_server_name=$do_server_name do_region=$do_region" -} - -ec2 () { - read -p " -Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). -$ADDITIONAL_PROMPT -[AKIA...]: " -rs aws_access_key - - read -p " - -Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -$ADDITIONAL_PROMPT -[ABCD...]: " -rs aws_secret_key - -read -p " - -Name the vpn server: -[algo]: " -r aws_server_name - aws_server_name=${aws_server_name:-algo} - - read -p " - - What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-east-2 US East (Ohio) - 3. us-west-1 US West (N. California) - 4. us-west-2 US West (Oregon) - 5. ca-central-1 Canada (Central) - 6. eu-central-1 EU (Frankfurt) - 7. eu-west-1 EU (Ireland) - 8. eu-west-2 EU (London) - 9. eu-west-3 EU (Paris) - 10. ap-northeast-1 Asia Pacific (Tokyo) - 11. ap-northeast-2 Asia Pacific (Seoul) - 12. ap-northeast-3 Asia Pacific (Osaka-Local) - 13. ap-southeast-1 Asia Pacific (Singapore) - 14. ap-southeast-2 Asia Pacific (Sydney) - 15. ap-south-1 Asia Pacific (Mumbai) - 16. sa-east-1 South America (São Paulo) - -Enter the number of your desired region: -[1]: " -r aws_region - aws_region=${aws_region:-1} - - case "$aws_region" in - 1) region="us-east-1" ;; - 2) region="us-east-2" ;; - 3) region="us-west-1" ;; - 4) region="us-west-2" ;; - 5) region="ca-central-1" ;; - 6) region="eu-central-1" ;; - 7) region="eu-west-1" ;; - 8) region="eu-west-2" ;; - 9) region="eu-west-3" ;; - 10) region="ap-northeast-1" ;; - 11) region="ap-northeast-2" ;; - 12) region="ap-northeast-3";; - 13) region="ap-southeast-1" ;; - 14) region="ap-southeast-2" ;; - 15) region="ap-south-1" ;; - 16) region="sa-east-1" ;; - esac - - ROLES="ec2 vpn cloud" - EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key aws_server_name=$aws_server_name region=$region" -} - -lightsail () { -read -p " -Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md). -$ADDITIONAL_PROMPT -[AKIA...]: " -rs aws_access_key - -read -p " - -Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) -$ADDITIONAL_PROMPT -[ABCD...]: " -rs aws_secret_key - -read -p " - -Name the vpn server: -[algo.local]: " -r algo_server_name - algo_server_name=${algo_server_name:-algo.local} - - read -p " - - What region should the server be located in? - 1. us-east-1 US East (N. Virginia) - 2. us-east-2 US East (Ohio) - 3. us-west-1 US West (N. California) - 4. us-west-2 US West (Oregon) - 5. ap-south-1 Asia Pacific (Mumbai) - 6. ap-northeast-2 Asia Pacific (Seoul) - 7. ap-southeast-1 Asia Pacific (Singapore) - 8. ap-southeast-2 Asia Pacific (Sydney) - 9. ap-northeast-1 Asia Pacific (Tokyo) - 10. eu-central-1 EU (Frankfurt) - 11. eu-west-1 EU (Ireland) - 12. eu-west-2 EU (London) - -Enter the number of your desired region: -[1]: " -r algo_region -algo_region=${algo_region:-1} - - case "$algo_region" in - 1) region="us-east-1" ;; - 2) region="us-east-2" ;; - 3) region="us-west-1" ;; - 4) region="us-west-2" ;; - 5) region="ap-south-1" ;; - 6) region="ap-northeast-2" ;; - 7) region="ap-southeast-1" ;; - 8) region="ap-southeast-2" ;; - 9) region="ap-northeast-1" ;; - 10) region="eu-central-1" ;; - 11) region="eu-west-1" ;; - 12) region="eu-west-2";; - esac - - ROLES="lightsail vpn cloud" - EXTRA_VARS="aws_access_key=$aws_access_key aws_secret_key=$aws_secret_key algo_server_name=$algo_server_name region=$region" -} - -scaleway () { -read -p " -Enter your auth token (https://www.scaleway.com/docs/generate-an-api-token/) -$ADDITIONAL_PROMPT -[...]: " -rs scaleway_auth_token - -read -p " - -Enter your organization name (https://cloud.scaleway.com/#/billing) -$ADDITIONAL_PROMPT -[...]: " -rs scaleway_organization - -read -p " - -Name the vpn server: -[algo.local]: " -r algo_server_name - algo_server_name=${algo_server_name:-algo.local} - - read -p " - - What region should the server be located in? - 1. par1 Paris - 2. ams1 Amsterdam -Enter the number of your desired region: -[1]: " -r algo_region -algo_region=${algo_region:-1} - - case "$algo_region" in - 1) region="par1" ;; - 2) region="ams1" ;; - esac - - ROLES="scaleway vpn cloud" - EXTRA_VARS="scaleway_auth_token=$scaleway_auth_token scaleway_organization=\"$scaleway_organization\" algo_server_name=$algo_server_name algo_region=$region" -} - -openstack () { -read -p " -Enter the local path to your credentials OpenStack RC file (Can be downloaded from the OpenStack dashboard->Compute->API Access) -[...]: " -r os_rc - -read -p " - -Name the vpn server: -[algo.local]: " -r algo_server_name - algo_server_name=${algo_server_name:-algo.local} - - ROLES="openstack vpn cloud" - EXTRA_VARS="algo_server_name=$algo_server_name" - source $os_rc -} - -gce () { - read -p " -Enter the local path to your credentials JSON file (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts): -: " -r credentials_file - - read -p " - -Name the vpn server: -[algo]: " -r server_name - server_name=${server_name:-algo} - - read -p " - - What zone should the server be located in? - 1. Eastern Canada (Montreal A) - 2. Eastern Canada (Montreal B) - 3. Eastern Canada (Montreal C) - 4. Central US (Iowa A) - 5. Central US (Iowa B) - 6. Central US (Iowa C) - 7. Central US (Iowa F) - 8. Western US (Oregon A) - 9. Western US (Oregon B) - 10. Western US (Oregon C) - 11. Eastern US (Northern Virginia A) - 12. Eastern US (Northern Virginia B) - 13. Eastern US (Northern Virginia C) - 14. Eastern US (South Carolina B) - 15. Eastern US (South Carolina C) - 16. Eastern US (South Carolina D) - 17. South America East (São Paulo A) - 18. South America East (São Paulo B) - 19. South America East (São Paulo C) - 20. Northern Europe (Hamina A) - 21. Northern Europe (Hamina B) - 22. Northern Europe (Hamina C) - 23. Western Europe (Belgium B) - 24. Western Europe (Belgium C) - 25. Western Europe (Belgium D) - 26. Western Europe (London A) - 27. Western Europe (London B) - 28. Western Europe (London C) - 29. Western Europe (Frankfurt A) - 30. Western Europe (Frankfurt B) - 31. Western Europe (Frankfurt C) - 32. Western Europe (Netherlands A) - 33. Western Europe (Netherlands B) - 34. Western Europe (Netherlands C) - 35. South Asia (Mumbai A) - 36. South Asia (Mumbai B) - 37. South Asia (Mumbai C) - 38. Southeast Asia (Singapore A) - 39. Southeast Asia (Singapore B) - 40. Southeast Asia (Singapore C) - 41. East Asia (Taiwan A) - 42. East Asia (Taiwan B) - 43. East Asia (Taiwan C) - 44. Northeast Asia (Tokyo A) - 45. Northeast Asia (Tokyo B) - 46. Northeast Asia (Tokyo C) - 47. Australia (Sydney A) - 48. Australia (Sydney B) - 49. Australia (Sydney C) - -Please choose the number of your zone. Press enter for default (#20) zone. -[20]: " -r region - region=${region:-20} - - case "$region" in - 1) zone="northamerica-northeast1-a" ;; - 2) zone="northamerica-northeast1-b" ;; - 3) zone="northamerica-northeast1-c" ;; - 4) zone="us-central1-a" ;; - 5) zone="us-central1-b" ;; - 6) zone="us-central1-c" ;; - 7) zone="us-central1-f" ;; - 8) zone="us-west1-a" ;; - 9) zone="us-west1-b" ;; - 10) zone="us-west1-c" ;; - 11) zone="us-east4-a" ;; - 12) zone="us-east4-b" ;; - 13) zone="us-east4-c" ;; - 14) zone="us-east1-b" ;; - 15) zone="us-east1-c" ;; - 16) zone="us-east1-d" ;; - 17) zone="southamerica-east1-a" ;; - 18) zone="southamerica-east1-b" ;; - 19) zone="southamerica-east1-c" ;; - 20) zone="europe-north1-a" ;; - 21) zone="europe-north1-b" ;; - 22) zone="europe-north1-c" ;; - 23) zone="europe-west1-b" ;; - 24) zone="europe-west1-c" ;; - 25) zone="europe-west1-d" ;; - 26) zone="europe-west2-a" ;; - 27) zone="europe-west2-b" ;; - 28) zone="europe-west2-c" ;; - 29) zone="europe-west3-a" ;; - 30) zone="europe-west3-b" ;; - 31) zone="europe-west3-c" ;; - 32) zone="europe-west4-a" ;; - 33) zone="europe-west4-b" ;; - 34) zone="europe-west4-c" ;; - 35) zone="asia-south1-a" ;; - 36) zone="asia-south1-b" ;; - 37) zone="asia-south1-c" ;; - 38) zone="asia-southeast1-a" ;; - 39) zone="asia-southeast1-b" ;; - 40) zone="asia-southeast1-c" ;; - 41) zone="asia-east1-a" ;; - 42) zone="asia-east1-b" ;; - 43) zone="asia-east1-c" ;; - 44) zone="asia-northeast1-a" ;; - 45) zone="asia-northeast1-b" ;; - 46) zone="asia-northeast1-c" ;; - 47) zone="australia-southeast1-a" ;; - 48) zone="australia-southeast1-b" ;; - 49) zone="australia-southeast1-c" ;; - esac - - ROLES="gce vpn cloud" - EXTRA_VARS="credentials_file=$credentials_file gce_server_name=$server_name ssh_public_key=$ssh_public_key zone=$zone max_mss=1316" -} - -non_cloud () { - read -p " -Enter the IP address of your server: (or use localhost for local installation) -[localhost]: " -r server_ip - server_ip=${server_ip:-localhost} - - read -p " - -What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) -[root]: " -r server_user - server_user=${server_user:-root} - -if [ "x${server_ip}" = "xlocalhost" ]; then - myip="" -else - myip=${server_ip} -fi - - read -p " - -Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$myip]: " -r IP_subject - IP_subject=${IP_subject:-$myip} - -if [ "x${IP_subject}" = "x" ]; then - echo "no server IP given. exiting." - exit 1 -fi - - ROLES="local vpn" - EXTRA_VARS="server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$IP_subject" - SKIP_TAGS+=" cloud update-alternatives" - - read -p " - -Was this server deployed by Algo previously? -[y/N]: " -r Deployed_By_Algo -Deployed_By_Algo=${Deployed_By_Algo:-n} -if [[ "$Deployed_By_Algo" =~ ^(y|Y)$ ]]; then EXTRA_VARS+=" Deployed_By_Algo=Y"; fi - -} - -algo_provisioning () { - echo -n " - What provider would you like to use? - 1. DigitalOcean - 2. Amazon EC2 - 3. Microsoft Azure - 4. Google Compute Engine - 5. Scaleway - 6. OpenStack (DreamCompute optimised) - 7. Install to existing Ubuntu 16.04 server (Advanced) - -Enter the number of your desired provider -: " - - read -r N - - case "$N" in - 1) digitalocean; ;; - 2) ec2; ;; - 3) azure; ;; - 4) gce; ;; - 5) scaleway; ;; - 6) openstack; ;; - 7) non_cloud; ;; - *) exit 1 ;; - esac - - additional_roles - deploy -} - -user_management () { - - read -p " -Enter the IP address of your server: (or use localhost for local installation) -: " -r server_ip - - read -p " -What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) -[root]: " -r server_user - server_user=${server_user:-root} - -read -p " -Do you want each user to have their own account for SSH tunneling? -[y/N]: " -r ssh_tunneling_enabled -ssh_tunneling_enabled=${ssh_tunneling_enabled:-n} - -if [ "x${server_ip}" = "xlocalhost" ]; then - myip="" -else - myip=${server_ip} -fi - -read -p " - -Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) -[$myip]: " -r IP_subject -IP_subject=${IP_subject:-$myip} - -if [ "x${IP_subject}" = "x" ]; then -echo "no server IP given. exiting." -exit 1 -fi - - read -p " -Enter the password for the private CA key: -$ADDITIONAL_PROMPT -: " -rs easyrsa_CA_password - -ansible-playbook users.yml -e "server_ip=$server_ip server_user=$server_user ssh_tunneling_enabled=$ssh_tunneling_enabled IP_subject_alt_name=$IP_subject easyrsa_CA_password=$easyrsa_CA_password" -t update-users --skip-tags common -} - case "$1" in - update-users) user_management ;; - *) algo_provisioning ;; + update-users) PLAYBOOK=users.yml; ARGS="${@:2} -t update-users";; + *) PLAYBOOK=main.yml; ARGS=${@} ;; esac + +ansible-playbook ${PLAYBOOK} ${ARGS} diff --git a/ansible.cfg b/ansible.cfg index c4d18d19..aef40841 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -4,6 +4,7 @@ pipelining = True retry_files_enabled = False host_key_checking = False timeout = 60 +stdout_callback = full_skip [paramiko_connection] record_host_keys = False diff --git a/cloud.yml b/cloud.yml new file mode 100644 index 00000000..3a4e299f --- /dev/null +++ b/cloud.yml @@ -0,0 +1,49 @@ +--- +- name: Provision the server + hosts: localhost + tags: algo + vars_files: + - config.cfg + + pre_tasks: + - block: + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml + tags: always + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always + + roles: + - role: cloud-digitalocean + when: algo_provider == "digitalocean" + - role: cloud-ec2 + when: algo_provider == "ec2" + - role: cloud-vultr + when: algo_provider == "vultr" + - role: cloud-gce + when: algo_provider == "gce" + - role: cloud-azure + when: algo_provider == "azure" + - role: cloud-lightsail + when: algo_provider == "lightsail" + - role: cloud-scaleway + when: algo_provider == "scaleway" + - role: cloud-openstack + when: algo_provider == "openstack" + - role: local + when: algo_provider == "local" + + post_tasks: + - block: + - name: Local post-tasks + import_tasks: playbooks/cloud-post.yml + become: false + tags: cloud + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/config.cfg b/config.cfg index a8fa915a..b5bbb9ca 100644 --- a/config.cfg +++ b/config.cfg @@ -10,8 +10,8 @@ users: ### Advanced users only below this line ### -# If True re-init all existing certificates. (True or False) -easyrsa_reinit_existent: False +# If True re-init all existing certificates. Boolean +keys_clean_all: False vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' @@ -28,9 +28,6 @@ wireguard_port: 51820 # - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan #max_mss: 1316 -server_name: "{{ ansible_ssh_host }}" -IP_subject_alt_name: "{{ ansible_ssh_host }}" - # StrongSwan log level # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 @@ -64,7 +61,7 @@ VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" # Block traffic between connected clients -BetweenClients_DROP: Y +BetweenClients_DROP: true congrats: common: | @@ -75,9 +72,9 @@ congrats: "# and ensure that all your traffic passes through the VPN. #" "# Local DNS resolver {{ local_service_ip }} #" p12_pass: | - "# The p12 and SSH keys password for new users is {{ easyrsa_p12_export_password }} #" + "# The p12 and SSH keys password for new users is {{ p12_export_password }} #" ca_key_pass: | - "# The CA key password is {{ easyrsa_CA_password }} #" + "# The CA key password is {{ CA_password }} #" ssh_access: | "# Shell access: ssh -i {{ ansible_ssh_private_key_file|default(omit) }} {{ ansible_ssh_user|default(omit) }}@{{ ansible_ssh_host|default(omit) }} #" @@ -98,6 +95,7 @@ cloud_providers: size: s-1vcpu-1gb image: "ubuntu-18-04-x64" ec2: + encrypted: false size: t2.micro image: name: "ubuntu-bionic-18.04" @@ -115,9 +113,16 @@ cloud_providers: openstack: flavor_ram: ">=512" image: Ubuntu-18.04 + vultr: + os: Ubuntu 18.04 x64 + size: 1024 MB RAM,25 GB SSD,1.00 TB BW local: fail_hint: - Sorry, but something went wrong! - Please check the troubleshooting guide. - https://trailofbits.github.io/algo/troubleshooting.html + +booleans_map: + Y: true + y: true diff --git a/deploy.yml b/deploy.yml deleted file mode 100644 index 532820c7..00000000 --- a/deploy.yml +++ /dev/null @@ -1,98 +0,0 @@ -- name: Configure the server - hosts: localhost - tags: algo - vars_files: - - config.cfg - - pre_tasks: - - block: - - name: Local pre-tasks - include_tasks: playbooks/local.yml - tags: [ 'always' ] - - - name: Local pre-tasks - include_tasks: playbooks/local_ssh.yml - become: false - when: Deployed_By_Algo is defined and Deployed_By_Algo == "Y" - tags: [ 'local' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - - roles: - - { role: cloud-digitalocean, tags: ['digitalocean'] } - - { role: cloud-ec2, tags: ['ec2'] } - - { role: cloud-gce, tags: ['gce'] } - - { role: cloud-azure, tags: ['azure'] } - - { role: cloud-scaleway, tags: ['scaleway'] } - - { role: cloud-openstack, tags: ['openstack'] } - - { role: local, tags: ['local'] } - - post_tasks: - - block: - - name: Local post-tasks - include_tasks: playbooks/post.yml - become: false - tags: [ 'cloud' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - -- name: Configure the server and install required software - hosts: vpn-host - gather_facts: false - tags: algo - become: true - vars_files: - - config.cfg - - pre_tasks: - - block: - - name: Common pre-tasks - include_tasks: playbooks/common.yml - tags: [ 'digitalocean', 'ec2', 'gce', 'azure', 'lightsail', 'scaleway', 'openstack', 'local', 'pre' ] - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always - - roles: - - { role: dns_adblocking, tags: [ 'dns', 'adblock' ] } - - { role: ssh_tunneling, tags: [ 'ssh_tunneling' ] } - - { role: wireguard, tags: [ 'vpn', 'wireguard' ], when: wireguard_enabled } - - { role: vpn, tags: [ 'vpn' ] } - - post_tasks: - - block: - - debug: - msg: - - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - - " {% if Store_CAKEY is defined and Store_CAKEY == 'N' %}{% else %}{{ congrats.ca_key_pass }}{% endif %}" - - " {% if cloud_deployment is defined %}{{ congrats.ssh_access }}{% endif %}" - tags: always - - - name: Save the CA key password - local_action: > - shell echo "{{ easyrsa_CA_password }}" > /tmp/ca_password - become: no - tags: tests - - - name: Delete the CA key - local_action: - module: file - path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" - state: absent - become: no - tags: always - when: Store_CAKEY is defined and Store_CAKEY == "N" - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always diff --git a/docs/cloud-do.md b/docs/cloud-do.md index b8f84681..675754a9 100644 --- a/docs/cloud-do.md +++ b/docs/cloud-do.md @@ -78,10 +78,10 @@ You will then be asked the remainder of the setup questions. ## Using DigitalOcean with Algo (via Ansible) -If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_access_token`. +If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_token`. For example, - ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2 + ansible-playbook deploy.yml -e 'provider=digitalocean do_token=my_secret_token' -Where "my_secret_token" is your API Token. +Where "my_secret_token" is your API Token. For more references see [deploy-from-ansible](deploy-from-ansible.md) diff --git a/docs/cloud-vultr.md b/docs/cloud-vultr.md new file mode 100644 index 00000000..3448e773 --- /dev/null +++ b/docs/cloud-vultr.md @@ -0,0 +1,8 @@ +### Configuration file + +You need to create a configuration file in INI format with your api key (https://my.vultr.com/settings/#settingsapi) + +``` +[default] +key = +``` diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index f7bcf6da..f3566c7f 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -11,74 +11,81 @@ You can deploy Algo non-interactively by running the Ansible playbooks directly Here is a full example for DigitalOcean: ```shell -ansible-playbook deploy.yml -t digitalocean,vpn,cloud -e 'do_access_token=my_secret_token do_server_name=algo.local do_region=ams2' +ansible-playbook main.yml -e "provider=digitalocean + server_name=algo + ondemand_cellular=false + ondemand_wifi=false + local_dns=true + ssh_tunneling=true + windows=false + store_cakey=true + region=ams3 + do_token=token" ``` +See below for more information about providers and extra variables + +### Variables + +- `provider` - (Required) The provider to use. See possible values below +- `server_name` - (Required) Server name. Default: algo +- `ondemand_cellular` (Optional) VPN On Demand when connected to cellular networks. Default: false +- `ondemand_wifi` - (Optional. See `ondemand_wifi_exclude`) VPN On Demand when connected to WiFi networks. Default: false +- `ondemand_wifi_exclude` (Required if `ondemand_wifi` set) - WiFi networks to exclude from using the VPN. Comma-separated values +- `local_dns` - (Optional) Enable a DNS resolver. Default: false +- `ssh_tunneling` - (Optional) Enable SSH tunneling for each user. Default: false +- `windows` - (Optional) Enables compatible ciphers and key exchange to support Windows clietns, less secure. Default: false +- `store_cakey` - (Optional) Whether or not keep the CA key (required to add users in the future, but less secure). Default: false + +If any of those unspecified ansible will ask the user to input + ### Ansible roles -Required tags: - -- cloud +Roles can be activated by specifying an extra variable `provider` Cloud roles: -- role: cloud-digitalocean, tags: digitalocean -- role: cloud-ec2, tags: ec2 -- role: cloud-gce, tags: gce +- role: cloud-digitalocean, provider: digitalocean +- role: cloud-ec2, provider: ec2 +- role: cloud-vultr, provider: vultr +- role: cloud-gce, provider: gce +- role: cloud-azure, provider: azure +- role: cloud-scaleway, provider: scaleway +- role: cloud-openstack, provider: openstack Server roles: -- role: vpn, tags: vpn -- role: dns_adblocking, tags: dns, adblock -- role: security, tags: security -- role: ssh_tunneling, tags: ssh_tunneling +- role: vpn +- role: dns_adblocking +- role: dns_encryption +- role: ssh_tunneling +- role: wireguard Note: The `vpn` role generates Apple profiles with On-Demand Wifi and Cellular if you pass the following variables: -- OnDemandEnabled_WIFI=Y -- OnDemandEnabled_WIFI_EXCLUDE=HomeNet -- OnDemandEnabled_Cellular=Y +- ondemand_wifi: true +- ondemand_wifi_exclude: HomeNet,OfficeWifi +- ondemand_cellular: true ### Local Installation -Required tags: - -- local +- role: local, provider: local Required variables: -- server_ip -- server_user -- IP_subject_alt_name +- server - IP address of your server +- ca_password - Password for the private CA key -Note that by default, the iptables rules on your existing server will be overwritten. If you don't want to overwrite the iptables rules, you can use the `--skip-tags iptables` flag, for example: - -```shell -ansible-playbook deploy.yml -t local,vpn --skip-tags iptables -e 'server_ip=172.217.2.238 server_user=algo IP_subject_alt_name=172.217.2.238' -``` +Note that by default, the iptables rules on your existing server will be overwritten. If you don't want to overwrite the iptables rules, you can use the `--skip-tags iptables` flag. ### Digital Ocean Required variables: -- do_access_token -- do_server_name -- do_region +- do_token +- region -Possible options for `do_region`: - -- ams2 -- ams3 -- fra1 -- lon1 -- nyc1 -- nyc2 -- nyc3 -- sfo1 -- sfo2 -- sgp1 -- tor1 -- blr1 +Possible options can be gathered calling to https://api.digitalocean.com/v2/regions ### Amazon EC2 @@ -86,27 +93,13 @@ Required variables: - aws_access_key - aws_secret_key -- aws_server_name - region -Possible options for `region`: +Possible options can be gathered via cli `aws ec2 describe-regions` -- us-east-1 -- us-east-2 -- us-west-1 -- us-west-2 -- ap-south-1 -- ap-northeast-2 -- ap-southeast-1 -- ap-southeast-2 -- ap-northeast-1 -- eu-central-1 -- eu-west-1 -- eu-west-2 +Additional variables: -Additional tags: - -- [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) (enabled by default) +- [encrypted](https://aws.amazon.com/blogs/aws/new-encrypted-ebs-boot-volumes/) - Encrypted EBS boot volume. Boolean (Default: false) #### Minimum required IAM permissions for deployment: @@ -178,46 +171,76 @@ Additional tags: Required variables: -- credentials_file -- gce_server_name -- ssh_public_key -- zone +- gce_credentials_file +- [region](https://cloud.google.com/compute/docs/regions-zones/) -Possible options for `zone`: +### Vultr -- us-west1-a -- us-west1-b -- us-west1-c -- us-central1-a -- us-central1-b -- us-central1-c -- us-central1-f -- us-east4-a -- us-east4-b -- us-east4-c -- us-east1-b -- us-east1-c -- us-east1-d -- europe-north1-a -- europe-north1-b -- europe-north1-c -- europe-west1-b -- europe-west1-c -- europe-west1-d -- europe-west2-a -- europe-west2-b -- europe-west2-c -- europe-west3-a -- europe-west3-b -- europe-west3-c -- asia-southeast1-a -- asia-southeast1-b -- asia-east1-a -- asia-east1-b -- asia-east1-c -- asia-northeast1-a -- asia-northeast1-b -- asia-northeast1-c -- australia-southeast1-a -- australia-southeast1-b -- australia-southeast1-c +Required variables: + +- [vultr_config](https://github.com/trailofbits/algo/docs/cloud-vultr.md) +- [region](https://api.vultr.com/v1/regions/list) + +### Azure + +Required variables: + +- azure_secret +- azure_tenant +- azure_client_id +- azure_subscription_id +- [region](https://azure.microsoft.com/en-us/global-infrastructure/regions/) + +### Lightsail + +Required variables: + +- aws_access_key +- aws_secret_key +- region + +Possible options can be gathered via cli `aws lightsail get-regions` + +### Scaleway + +Required variables: + +- [scaleway_token](https://www.scaleway.com/docs/generate-an-api-token/) +- [scaleway_org](https://cloud.scaleway.com/#/billing) +- region + +Possible regions: + +- ams1 +- par1 + +### OpenStack + +You need to source the rc file prior to run Algo. Download it from the OpenStack dashboard->Compute->API Access and source it in the shell (eg: source /tmp/dhc-openrc.sh) + + +### Local + +Required variables: + +- server - IP or hostname to access the server via SSH +- endpoint - Public IP address of your server +- ssh_user + + +### Update users + +Playbook: + +``` +users.yml +``` + +Required variables: + +- server - IP or hostname to access the server via SSH +- ca_password - Password to access the CA key + +Tags required: + +- update-users diff --git a/docs/deploy-to-freebsd.md b/docs/deploy-to-freebsd.md index 71440cc5..a0c04d4c 100644 --- a/docs/deploy-to-freebsd.md +++ b/docs/deploy-to-freebsd.md @@ -26,5 +26,7 @@ device crypto ## Installation ```shell -ansible-playbook deploy.yml -t local,vpn -e "server_ip=$server_ip server_user=$server_user IP_subject_alt_name=$server_ip Store_CAKEY=N" --skip-tags cloud +ansible-playbook main.yml -e "provider=local" ``` + +And follow the instructions diff --git a/docs/index.md b/docs/index.md index 47705b7a..84f07185 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ * Cloud setup - Configure [Azure](cloud-azure.md) - Configure [DigitalOcean](cloud-do.md) + - Configure [Vultr](cloud-vultr.md) * Advanced Deployment - Deploy to your own [FreeBSD](deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 18.04](deploy-to-ubuntu.md) server diff --git a/input.yml b/input.yml new file mode 100644 index 00000000..aeb53192 --- /dev/null +++ b/input.yml @@ -0,0 +1,137 @@ +--- +- name: Ask user for the input + hosts: localhost + tags: algo + vars: + defaults: + server_name: algo + ondemand_cellular: false + ondemand_wifi: false + local_dns: false + ssh_tunneling: false + windows: false + store_cakey: false + providers_map: + - { name: DigitalOcean, alias: digitalocean } + - { name: Amazon EC2, alias: ec2 } + - { name: Vultr, alias: vultr } + - { name: Microsoft Azure, alias: azure } + - { name: Google Compute Engine, alias: gce } + - { name: Scaleway, alias: scaleway} + - { name: OpenStack (DreamCompute optimised), alias: openstack } + - { name: Install to existing Ubuntu 18.04 server (Advanced), alias: local } + vars_files: + - config.cfg + + tasks: + - pause: + prompt: | + What provider would you like to use? + {% for p in providers_map %} + {{ loop.index }}. {{ p['name']}} + {% endfor %} + + Enter the number of your desired provider + register: _algo_provider + when: provider is undefined + + - name: Set facts based on the input + set_fact: + algo_provider: "{{ provider | default(providers_map[_algo_provider.user_input|default(omit)|int - 1]['alias']) }}" + + - pause: + prompt: | + Name the vpn server + [algo] + register: _algo_server_name + when: + - server_name is undefined + - algo_provider != "local" + + - pause: + prompt: | + Do you want macOS/iOS clients to enable "VPN On Demand" when connected to cellular networks? + [y/N] + register: _ondemand_cellular + when: ondemand_cellular is undefined + + - pause: + prompt: | + Do you want macOS/iOS clients to enable "VPN On Demand" when connected to Wi-Fi? + [y/N] + register: _ondemand_wifi + when: ondemand_wifi is undefined + + - pause: + prompt: | + List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN + (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) + register: _ondemand_wifi_exclude + when: + - ondemand_wifi_exclude is undefined + - (ondemand_wifi|default(false)|bool) or + (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) + + - pause: + prompt: | + Do you want to install a DNS resolver on this VPN server, to block ads while surfing? + [y/N] + register: _local_dns + when: local_dns is undefined + + - pause: + prompt: | + Do you want each user to have their own account for SSH tunneling? + [y/N] + register: _ssh_tunneling + when: ssh_tunneling is undefined + + - pause: + prompt: | + Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) + [y/N] + register: _windows + when: windows is undefined + + - pause: + prompt: | + Do you want to retain the CA key? (required to add users in the future, but less secure) + [y/N] + register: _store_cakey + when: store_cakey is undefined + + - name: Set facts based on the input + set_fact: + algo_server_name: >- + {% if server_name is defined %}{% set _server = server_name %} + {%- elif _algo_server_name.user_input is defined and _algo_server_name.user_input != "" %}{% set _server = _algo_server_name.user_input %} + {%- else %}{% set _server = defaults['server_name'] %}{% endif -%} + {{ _server | regex_replace('(?!\.)(\W|_)', '-') }} + algo_ondemand_cellular: >- + {% if ondemand_cellular is defined %}{{ ondemand_cellular | bool }} + {%- elif _ondemand_cellular.user_input is defined and _ondemand_cellular.user_input != "" %}{{ booleans_map[_ondemand_cellular.user_input] | default(defaults['ondemand_cellular']) }} + {%- else %}false{% endif %} + algo_ondemand_wifi: >- + {% if ondemand_wifi is defined %}{{ ondemand_wifi | bool }} + {%- elif _ondemand_wifi.user_input is defined and _ondemand_wifi.user_input != "" %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} + {%- else %}false{% endif %} + algo_ondemand_wifi_exclude: >- + {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input }} + {%- else %}_null{% endif %} + algo_local_dns: >- + {% if local_dns is defined %}{{ local_dns | bool }} + {%- elif _local_dns.user_input is defined and _local_dns.user_input != "" %}{{ booleans_map[_local_dns.user_input] | default(defaults['local_dns']) }} + {%- else %}false{% endif %} + algo_ssh_tunneling: >- + {% if ssh_tunneling is defined %}{{ ssh_tunneling | bool }} + {%- elif _ssh_tunneling.user_input is defined and _ssh_tunneling.user_input != "" %}{{ booleans_map[_ssh_tunneling.user_input] | default(defaults['ssh_tunneling']) }} + {%- else %}false{% endif %} + algo_windows: >- + {% if windows is defined %}{{ windows | bool }} + {%- elif _windows.user_input is defined and _windows.user_input != "" %}{{ booleans_map[_windows.user_input] | default(defaults['windows']) }} + {%- else %}false{% endif %} + algo_store_cakey: >- + {% if store_cakey is defined %}{{ store_cakey | bool }} + {%- elif _store_cakey.user_input is defined and _store_cakey.user_input != "" %}{{ booleans_map[_store_cakey.user_input] | default(defaults['store_cakey']) }} + {%- else %}false{% endif %} diff --git a/library/digital_ocean_tag.py b/library/digital_ocean_tag.py deleted file mode 100644 index 30a31852..00000000 --- a/library/digital_ocean_tag.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: digital_ocean_tag -short_description: Create and remove tag(s) to DigitalOcean resource. -description: - - Create and remove tag(s) to DigitalOcean resource. -author: "Victor Volle (@kontrafiktion)" -version_added: "2.2" -options: - name: - description: - - The name of the tag. The supported characters for names include - alphanumeric characters, dashes, and underscores. - required: true - resource_id: - description: - - The ID of the resource to operate on. - - The data type of resource_id is changed from integer to string, from version 2.5. - aliases: ['droplet_id'] - resource_type: - description: - - The type of resource to operate on. Currently, only tagging of - droplets is supported. - default: droplet - choices: ['droplet'] - state: - description: - - Whether the tag should be present or absent on the resource. - default: present - choices: ['present', 'absent'] - api_token: - description: - - DigitalOcean api token. - -notes: - - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. - They both refer to the v2 token. - - As of Ansible 2.0, Version 2 of the DigitalOcean API is used. - -requirements: - - "python >= 2.6" -''' - - -EXAMPLES = ''' -- name: create a tag - digital_ocean_tag: - name: production - state: present - -- name: tag a resource; creating the tag if it does not exists - digital_ocean_tag: - name: "{{ item }}" - resource_id: "73333005" - state: present - with_items: - - staging - - dbserver - -- name: untag a resource - digital_ocean_tag: - name: staging - resource_id: "73333005" - state: absent - -# Deleting a tag also untags all the resources that have previously been -# tagged with it -- name: remove a tag - digital_ocean_tag: - name: dbserver - state: absent -''' - - -RETURN = ''' -data: - description: a DigitalOcean Tag resource - returned: success and no resource constraint - type: dict - sample: { - "tag": { - "name": "awesome", - "resources": { - "droplets": { - "count": 0, - "last_tagged": null - } - } - } - } -''' - -from traceback import format_exc -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.digital_ocean import DigitalOceanHelper -from ansible.module_utils._text import to_native - - -def core(module): - state = module.params['state'] - name = module.params['name'] - resource_id = module.params['resource_id'] - resource_type = module.params['resource_type'] - - rest = DigitalOceanHelper(module) - - # Check if api_token is valid or not - response = rest.get('account') - if response.status_code == 401: - module.fail_json(msg='Failed to login using api_token, please verify ' - 'validity of api_token') - if state == 'present': - response = rest.get('tags/{0}'.format(name)) - status_code = response.status_code - resp_json = response.json - changed = False - if status_code == 200 and resp_json['tag']['name'] == name: - changed = False - else: - # Ensure Tag exists - response = rest.post("tags", data={'name': name}) - status_code = response.status_code - resp_json = response.json - if status_code == 201: - changed = True - elif status_code == 422: - changed = False - else: - module.exit_json(changed=False, data=resp_json) - - if resource_id is None: - # No resource defined, we're done. - module.exit_json(changed=changed, data=resp_json) - else: - # Check if resource is already tagged or not - found = False - url = "{0}?tag_name={1}".format(resource_type, name) - if resource_type == 'droplet': - url = "droplets?tag_name={0}".format(name) - response = rest.get(url) - status_code = response.status_code - resp_json = response.json - if status_code == 200: - for resource in resp_json['droplets']: - if not found and resource['id'] == int(resource_id): - found = True - break - if not found: - # If resource is not tagged, tag a resource - url = "tags/{0}/resources".format(name) - payload = { - 'resources': [{ - 'resource_id': resource_id, - 'resource_type': resource_type}]} - response = rest.post(url, data=payload) - if response.status_code == 204: - module.exit_json(changed=True) - else: - module.fail_json(msg="error tagging resource '{0}': {1}".format(resource_id, response.json["message"])) - else: - # Already tagged resource - module.exit_json(changed=False) - else: - # Unable to find resource specified by user - module.fail_json(msg=resp_json['message']) - - elif state == 'absent': - if resource_id: - url = "tags/{0}/resources".format(name) - payload = { - 'resources': [{ - 'resource_id': resource_id, - 'resource_type': resource_type}]} - response = rest.delete(url, data=payload) - else: - url = "tags/{0}".format(name) - response = rest.delete(url) - if response.status_code == 204: - module.exit_json(changed=True) - else: - module.exit_json(changed=False, data=response.json) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - name=dict(type='str', required=True), - resource_id=dict(aliases=['droplet_id'], type='str'), - resource_type=dict(choices=['droplet'], default='droplet'), - state=dict(choices=['present', 'absent'], default='present'), - api_token=dict(aliases=['API_TOKEN'], no_log=True), - ) - ) - - try: - core(module) - except Exception as e: - module.fail_json(msg=to_native(e), exception=format_exc()) - - -if __name__ == '__main__': - main() diff --git a/library/ec2_ami_copy.py b/library/ec2_ami_copy.py deleted file mode 100644 index 629a48c6..00000000 --- a/library/ec2_ami_copy.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# -# Ansible 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. -# -# Ansible 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 Ansible. If not, see . - -ANSIBLE_METADATA = {'status': ['preview'], - 'supported_by': 'community', - 'version': '1.1'} - -DOCUMENTATION = ''' ---- -module: ec2_ami_copy -short_description: copies AMI between AWS regions, return new image id -description: - - Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5 -version_added: "2.0" -options: - source_region: - description: - - the source region that AMI should be copied from - required: true - source_image_id: - description: - - the id of the image in source region that should be copied - required: true - name: - description: - - The name of the new image to copy - required: true - default: null - description: - description: - - An optional human-readable string describing the contents and purpose of the new AMI. - required: false - default: null - encrypted: - description: - - Whether or not to encrypt the target image - required: false - default: null - version_added: "2.2" - kms_key_id: - description: - - KMS key id used to encrypt image. If not specified, uses default EBS Customer Master Key (CMK) for your account. - required: false - default: null - version_added: "2.2" - wait: - description: - - wait for the copied AMI to be in state 'available' before returning. - required: false - default: false - tags: - description: - - a hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}' - required: false - default: null - -author: Amir Moulavi , Tim C -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Basic AMI Copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - -# AMI copy wait until available -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - wait: yes - register: image_id - -# Named AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - name: My-Awesome-AMI - description: latest patch - -# Tagged AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - tags: - Name: My-Super-AMI - Patch: 1.2.3 - -# Encrypted AMI copy -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - -# Encrypted AMI copy with specified key -- ec2_ami_copy: - source_region: us-east-1 - region: eu-west-1 - source_image_id: ami-xxxxxxx - encrypted: yes - kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, get_aws_connection_info) - -try: - import boto - import boto.ec2 - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -try: - import boto3 - from botocore.exceptions import ClientError, NoCredentialsError, NoRegionError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - - - -def copy_image(ec2, module): - """ - Copies an AMI - - module : AnsibleModule object - ec2: ec2 connection object - """ - - tags = module.params.get('tags') - - params = {'SourceRegion': module.params.get('source_region'), - 'SourceImageId': module.params.get('source_image_id'), - 'Name': module.params.get('name'), - 'Description': module.params.get('description'), - 'Encrypted': module.params.get('encrypted'), -# 'KmsKeyId': module.params.get('kms_key_id') - } - if module.params.get('kms_key_id'): - params['KmsKeyId'] = module.params.get('kms_key_id') - - try: - image_id = ec2.copy_image(**params)['ImageId'] - if module.params.get('wait'): - ec2.get_waiter('image_available').wait(ImageIds=[image_id]) - if module.params.get('tags'): - ec2.create_tags( - Resources=[image_id], - Tags=[{'Key' : k, 'Value': v} for k,v in module.params.get('tags').items()] - ) - - module.exit_json(changed=True, image_id=image_id) - except ClientError as ce: - module.fail_json(msg=ce) - except NoCredentialsError: - module.fail_json(msg="Unable to locate AWS credentials") - except Exception as e: - module.fail_json(msg=str(e)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - source_region=dict(required=True), - source_image_id=dict(required=True), - name=dict(required=True), - description=dict(default=''), - encrypted=dict(type='bool', required=False), - kms_key_id=dict(type='str', required=False), - wait=dict(type='bool', default=False, required=False), - tags=dict(type='dict'))) - - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - # TODO: Check botocore version - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if HAS_BOTO3: - - try: - ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, - **aws_connect_params) - except NoRegionError: - module.fail_json(msg='AWS Region is required') - else: - module.fail_json(msg='boto3 required for this module') - - copy_image(ec2, module) - - -if __name__ == '__main__': - main() diff --git a/library/gce_region_facts.py b/library/gce_region_facts.py new file mode 100644 index 00000000..65acfb63 --- /dev/null +++ b/library/gce_region_facts.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# Copyright 2013 Google Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: gce_region_facts +version_added: "5.3" +short_description: Gather facts about GCE regions. +description: + - Gather facts about GCE regions. +options: + service_account_email: + version_added: "1.6" + description: + - service account email + required: false + default: null + aliases: [] + pem_file: + version_added: "1.6" + description: + - path to the pem file associated with the service account email + This option is deprecated. Use 'credentials_file'. + required: false + default: null + aliases: [] + credentials_file: + version_added: "2.1.0" + description: + - path to the JSON file associated with the service account email + required: false + default: null + aliases: [] + project_id: + version_added: "1.6" + description: + - your GCE project ID + required: false + default: null + aliases: [] + requirements: + - "python >= 2.6" + - "apache-libcloud >= 0.13.3, >= 0.17.0 if using JSON credentials" +author: "Jack Ivanov (@jackivanov)" +''' + +EXAMPLES = ''' +# Gather facts about all regions +- gce_region_facts: +''' + +RETURN = ''' +regions: + returned: on success + description: > + Each element consists of a dict with all the information related + to that region. + type: list + sample: "[{ + "name": "asia-east1", + "status": "UP", + "zones": [ + { + "name": "asia-east1-a", + "status": "UP" + }, + { + "name": "asia-east1-b", + "status": "UP" + }, + { + "name": "asia-east1-c", + "status": "UP" + } + ] + }]" +''' +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, ResourceExistsError, ResourceNotFoundError + _ = Provider.GCE + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.gce import gce_connect, unexpected_error_msg + + +def main(): + module = AnsibleModule( + argument_spec=dict( + service_account_email=dict(), + pem_file=dict(type='path'), + credentials_file=dict(type='path'), + project_id=dict(), + ) + ) + + if not HAS_LIBCLOUD: + module.fail_json(msg='libcloud with GCE support (0.17.0+) required for this module') + + gce = gce_connect(module) + + changed = False + gce_regions = [] + + try: + regions = gce.ex_list_regions() + for r in regions: + gce_region = {} + gce_region['name'] = r.name + gce_region['status'] = r.status + gce_region['zones'] = [] + for z in r.zones: + gce_zone = {} + gce_zone['name'] = z.name + gce_zone['status'] = z.status + gce_region['zones'].append(gce_zone) + gce_regions.append(gce_region) + json_output = { 'regions': gce_regions } + module.exit_json(changed=False, results=json_output) + except ResourceNotFoundError: + pass + + +if __name__ == '__main__': + main() diff --git a/library/lightsail_region_facts.py b/library/lightsail_region_facts.py new file mode 100644 index 00000000..8da4c00c --- /dev/null +++ b/library/lightsail_region_facts.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: lightsail_region_facts +short_description: Gather facts about AWS Lightsail regions. +description: + - Gather facts about AWS Lightsail regions. +version_added: "2.5.3" +author: "Jack Ivanov (@jackivanov)" +options: +requirements: + - "python >= 2.6" + - boto3 + +extends_documentation_fragment: + - aws + - ec2 +''' + + +EXAMPLES = ''' +# Gather facts about all regions +- lightsail_region_facts: +''' + +RETURN = ''' +regions: + returned: on success + description: > + Each element consists of a dict with all the information related + to that region. + type: list + sample: "[{ + "availabilityZones": [], + "continentCode": "NA", + "description": "This region is recommended to serve users in the eastern United States", + "displayName": "Virginia", + "name": "us-east-1" + }]" +''' + +import time +import traceback + +try: + import botocore + HAS_BOTOCORE = True +except ImportError: + HAS_BOTOCORE = False + +try: + import boto3 +except ImportError: + # will be caught by imported HAS_BOTO3 + pass + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, + HAS_BOTO3, camel_dict_to_snake_dict) + +def main(): + argument_spec = ec2_argument_spec() + module = AnsibleModule(argument_spec=argument_spec) + + if not HAS_BOTO3: + module.fail_json(msg='Python module "boto3" is missing, please install it') + + if not HAS_BOTOCORE: + module.fail_json(msg='Python module "botocore" is missing, please install it') + + try: + region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) + + client = None + try: + client = boto3_conn(module, conn_type='client', resource='lightsail', + region=region, endpoint=ec2_url, **aws_connect_kwargs) + except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: + module.fail_json(msg='Failed while connecting to the lightsail service: %s' % e, exception=traceback.format_exc()) + + response = client.get_regions( + includeAvailabilityZones=False + ) + module.exit_json(changed=False, results=response) + except (botocore.exceptions.ClientError, Exception) as e: + module.fail_json(msg=str(e), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/main.yml b/main.yml new file mode 100644 index 00000000..faf4c2d1 --- /dev/null +++ b/main.yml @@ -0,0 +1,9 @@ +--- +- name: Include prompts playbook + import_playbook: input.yml + +- name: Include cloud provisioning playbook + import_playbook: cloud.yml + +- name: Include server configuration playbook + import_playbook: server.yml diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml new file mode 100644 index 00000000..283ed60a --- /dev/null +++ b/playbooks/cloud-post.yml @@ -0,0 +1,45 @@ +--- +- name: Set subjectAltName as afact + set_fact: + IP_subject_alt_name: "{% if algo_provider == 'local' %}{{ IP_subject_alt_name }}{% else %}{{ cloud_instance_ip }}{% endif %}" + +- name: Add the server to an inventory group + add_host: + name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + groups: vpn-host + ansible_connection: "{% if cloud_instance_ip == 'localhost' %}local{% else %}ssh{% endif %}" + ansible_ssh_user: "{{ ansible_ssh_user }}" + ansible_python_interpreter: "/usr/bin/python2.7" + algo_provider: "{{ algo_provider }}" + algo_server_name: "{{ algo_server_name }}" + algo_ondemand_cellular: "{{ algo_ondemand_cellular }}" + algo_ondemand_wifi: "{{ algo_ondemand_wifi }}" + algo_ondemand_wifi_exclude: "{{ algo_ondemand_wifi_exclude }}" + algo_local_dns: "{{ algo_local_dns }}" + algo_ssh_tunneling: "{{ algo_ssh_tunneling }}" + algo_windows: "{{ algo_windows }}" + algo_store_cakey: "{{ algo_store_cakey }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Additional variables for the server + add_host: + name: "{% if cloud_instance_ip == 'localhost' %}localhost{% else %}{{ cloud_instance_ip }}{% endif %}" + ansible_ssh_private_key_file: "{{ SSH_keys.private }}" + when: algo_provider != 'local' + +- name: Wait until SSH becomes ready... + wait_for: + port: 22 + host: "{{ cloud_instance_ip }}" + search_regex: "OpenSSH" + delay: 10 + timeout: 320 + state: present + when: cloud_instance_ip != "localhost" + +- debug: + var: IP_subject_alt_name + +- name: A short pause, in order to be sure the instance is ready + pause: + seconds: 20 diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml new file mode 100644 index 00000000..da08b357 --- /dev/null +++ b/playbooks/cloud-pre.yml @@ -0,0 +1,13 @@ +--- +- name: Generate the SSH private key + openssl_privatekey: + path: "{{ SSH_keys.private }}" + size: 2048 + mode: "0600" + type: RSA + +- name: Generate the SSH public key + openssl_publickey: + path: "{{ SSH_keys.public }}" + privatekey_path: "{{ SSH_keys.private }}" + format: OpenSSH diff --git a/playbooks/common.yml b/playbooks/common.yml deleted file mode 100644 index e0aea2bb..00000000 --- a/playbooks/common.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- - -- name: Check the system - raw: uname -a - register: OS - -- name: Ubuntu pre-tasks - include_tasks: ubuntu.yml - when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' - -- name: FreeBSD pre-tasks - include_tasks: freebsd.yml - when: '"FreeBSD" in OS.stdout' - -- include_tasks: facts/main.yml diff --git a/playbooks/facts/FreeBSD.yml b/playbooks/facts/FreeBSD.yml deleted file mode 100644 index 0d025fc0..00000000 --- a/playbooks/facts/FreeBSD.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- - -- set_fact: - config_prefix: "/usr/local/" - root_group: wheel - ssh_service_name: sshd - apparmor_enabled: false - strongswan_additional_plugins: - - kernel-pfroute - - kernel-pfkey diff --git a/playbooks/facts/main.yml b/playbooks/facts/main.yml deleted file mode 100644 index a03e7810..00000000 --- a/playbooks/facts/main.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- - -- name: Gather Facts - setup: - -- name: Ensure the algo ssh key exist on the server - authorized_key: - user: "{{ ansible_ssh_user }}" - state: present - key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - tags: [ 'cloud' ] - -- name: Check if IPv6 configured - set_fact: - ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" - -- name: Set facts if the deployment in a cloud - set_fact: - cloud_deployment: true - tags: ['cloud'] - -- name: Generate password for the CA key - local_action: - module: shell - openssl rand -hex 16 - become: no - register: CA_password - -- name: Generate p12 export password - local_action: - module: shell - openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' - become: no - register: p12_export_password_generated - when: p12_export_password is not defined - -- name: Define password facts - set_fact: - easyrsa_p12_export_password: "{{ p12_export_password|default(p12_export_password_generated.stdout) }}" - easyrsa_CA_password: "{{ CA_password.stdout }}" - -- name: Define the commonName - set_fact: - IP_subject_alt_name: "{{ IP_subject_alt_name }}" diff --git a/playbooks/freebsd.yml b/playbooks/freebsd.yml deleted file mode 100644 index 316c92ac..00000000 --- a/playbooks/freebsd.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- - -- name: FreeBSD / HardenedBSD | Install prerequisites - raw: sleep 10 && env ASSUME_ALWAYS_YES=YES sudo pkg install -y python27 - -- name: FreeBSD / HardenedBSD | Configure defaults - raw: sudo ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7 - -- include_tasks: facts/FreeBSD.yml diff --git a/playbooks/local.yml b/playbooks/local.yml deleted file mode 100644 index 98a15774..00000000 --- a/playbooks/local.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- - -- name: Generate the SSH private key - shell: > - echo -e 'n' | - ssh-keygen -b 2048 -C {{ SSH_keys.comment }} - -t rsa -f {{ SSH_keys.private }} -q -N "" - args: - creates: "{{ SSH_keys.private }}" - -- name: Generate the SSH public key - shell: > - echo `ssh-keygen -y -f {{ SSH_keys.private }}` {{ SSH_keys.comment }} - > {{ SSH_keys.public }} - changed_when: false - -- name: Change mode for the SSH private key - file: - path: "{{ SSH_keys.private }}" - mode: 0600 - -- name: Ensure the dynamic inventory exists - blockinfile: - dest: configs/inventory.dynamic - marker: "# {mark} ALGO MANAGED BLOCK" - create: true - block: | - [algo:children] - {% for group in cloud_providers.keys() %} - {{ group }} - {% endfor %} diff --git a/playbooks/local_ssh.yml b/playbooks/local_ssh.yml deleted file mode 100644 index b2b30b77..00000000 --- a/playbooks/local_ssh.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- - -- name: Ensure the local ssh directory is exist - file: - path: ~/.ssh/ - state: directory - -- name: Copy the algo ssh key to the local ssh directory - copy: - src: "{{ SSH_keys.private }}" - dest: ~/.ssh/algo.pem - mode: '0600' diff --git a/playbooks/post.yml b/playbooks/post.yml deleted file mode 100644 index e594b973..00000000 --- a/playbooks/post.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- - -- name: Wait until SSH becomes ready... - wait_for: - port: 22 - host: "{{ cloud_instance_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present - -- name: A short pause, in order to be sure the instance is ready - pause: - seconds: 20 - -- include_tasks: local_ssh.yml diff --git a/playbooks/ubuntu.yml b/playbooks/ubuntu.yml deleted file mode 100644 index bf7ac5b5..00000000 --- a/playbooks/ubuntu.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- - -- name: Ubuntu | Install prerequisites - raw: "{{ item }}" - with_items: - - sleep 10 - - apt-get update -qq - - apt-get install -qq -y python2.7 sudo - become: true - -- name: Ubuntu | Configure defaults - raw: sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 - tags: - - update-alternatives diff --git a/requirements.txt b/requirements.txt index dae2ab65..f2580658 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ setuptools>=11.3 SecretStorage < 3 -ansible[azure]==2.4.3 +ansible[azure]==2.5.2 dopy==0.3.5 boto>=2.5 boto3 diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 0a3eedce..60fafed2 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -2,7 +2,7 @@ setup: - name: Include system based facts and tasks - include_tasks: systems/main.yml + import_tasks: systems/main.yml - name: Install prerequisites package: name="{{ item }}" state=present diff --git a/roles/cloud-azure/defaults/main.yml b/roles/cloud-azure/defaults/main.yml new file mode 100644 index 00000000..9170a157 --- /dev/null +++ b/roles/cloud-azure/defaults/main.yml @@ -0,0 +1,214 @@ +--- +azure_regions: > + [ + { + "displayName": "East Asia", + "latitude": "22.267", + "longitude": "114.188", + "name": "eastasia", + "subscriptionId": null + }, + { + "displayName": "Southeast Asia", + "latitude": "1.283", + "longitude": "103.833", + "name": "southeastasia", + "subscriptionId": null + }, + { + "displayName": "Central US", + "latitude": "41.5908", + "longitude": "-93.6208", + "name": "centralus", + "subscriptionId": null + }, + { + "displayName": "East US", + "latitude": "37.3719", + "longitude": "-79.8164", + "name": "eastus", + "subscriptionId": null + }, + { + "displayName": "East US 2", + "latitude": "36.6681", + "longitude": "-78.3889", + "name": "eastus2", + "subscriptionId": null + }, + { + "displayName": "West US", + "latitude": "37.783", + "longitude": "-122.417", + "name": "westus", + "subscriptionId": null + }, + { + "displayName": "North Central US", + "latitude": "41.8819", + "longitude": "-87.6278", + "name": "northcentralus", + "subscriptionId": null + }, + { + "displayName": "South Central US", + "latitude": "29.4167", + "longitude": "-98.5", + "name": "southcentralus", + "subscriptionId": null + }, + { + "displayName": "North Europe", + "latitude": "53.3478", + "longitude": "-6.2597", + "name": "northeurope", + "subscriptionId": null + }, + { + "displayName": "West Europe", + "latitude": "52.3667", + "longitude": "4.9", + "name": "westeurope", + "subscriptionId": null + }, + { + "displayName": "Japan West", + "latitude": "34.6939", + "longitude": "135.5022", + "name": "japanwest", + "subscriptionId": null + }, + { + "displayName": "Japan East", + "latitude": "35.68", + "longitude": "139.77", + "name": "japaneast", + "subscriptionId": null + }, + { + "displayName": "Brazil South", + "latitude": "-23.55", + "longitude": "-46.633", + "name": "brazilsouth", + "subscriptionId": null + }, + { + "displayName": "Australia East", + "latitude": "-33.86", + "longitude": "151.2094", + "name": "australiaeast", + "subscriptionId": null + }, + { + "displayName": "Australia Southeast", + "latitude": "-37.8136", + "longitude": "144.9631", + "name": "australiasoutheast", + "subscriptionId": null + }, + { + "displayName": "South India", + "latitude": "12.9822", + "longitude": "80.1636", + "name": "southindia", + "subscriptionId": null + }, + { + "displayName": "Central India", + "latitude": "18.5822", + "longitude": "73.9197", + "name": "centralindia", + "subscriptionId": null + }, + { + "displayName": "West India", + "latitude": "19.088", + "longitude": "72.868", + "name": "westindia", + "subscriptionId": null + }, + { + "displayName": "Canada Central", + "latitude": "43.653", + "longitude": "-79.383", + "name": "canadacentral", + "subscriptionId": null + }, + { + "displayName": "Canada East", + "latitude": "46.817", + "longitude": "-71.217", + "name": "canadaeast", + "subscriptionId": null + }, + { + "displayName": "UK South", + "latitude": "50.941", + "longitude": "-0.799", + "name": "uksouth", + "subscriptionId": null + }, + { + "displayName": "UK West", + "latitude": "53.427", + "longitude": "-3.084", + "name": "ukwest", + "subscriptionId": null + }, + { + "displayName": "West Central US", + "latitude": "40.890", + "longitude": "-110.234", + "name": "westcentralus", + "subscriptionId": null + }, + { + "displayName": "West US 2", + "latitude": "47.233", + "longitude": "-119.852", + "name": "westus2", + "subscriptionId": null + }, + { + "displayName": "Korea Central", + "latitude": "37.5665", + "longitude": "126.9780", + "name": "koreacentral", + "subscriptionId": null + }, + { + "displayName": "Korea South", + "latitude": "35.1796", + "longitude": "129.0756", + "name": "koreasouth", + "subscriptionId": null + }, + { + "displayName": "France Central", + "latitude": "46.3772", + "longitude": "2.3730", + "name": "francecentral", + "subscriptionId": null + }, + { + "displayName": "France South", + "latitude": "43.8345", + "longitude": "2.1972", + "name": "francesouth", + "subscriptionId": null + }, + { + "displayName": "Australia Central", + "latitude": "-35.3075", + "longitude": "149.1244", + "name": "australiacentral", + "subscriptionId": null + }, + { + "displayName": "Australia Central 2", + "latitude": "-35.3075", + "longitude": "149.1244", + "name": "australiacentral2", + "subscriptionId": null + } + ] diff --git a/roles/cloud-azure/handlers/main.yml b/roles/cloud-azure/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 6a6e9de4..682fcb3c 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,5 +1,8 @@ --- - block: + - name: Include prompts + import_tasks: prompts.yml + - set_fact: resource_group: "Algo_{{ region }}" secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" @@ -116,31 +119,10 @@ subnet_name: algo_subnet security_group_name: AlgoSecGroup - - name: Add the instance to an inventory group - add_host: - name: "{{ ip_address }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: azure - - set_fact: cloud_instance_ip: "{{ ip_address }}" + ansible_ssh_user: ubuntu - - name: Ensure the group azure exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[azure]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[azure\]' - regexp: "^{{ cloud_instance_ip }}.*" - line: "{{ cloud_instance_ip }}" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml new file mode 100644 index 00000000..aadffd61 --- /dev/null +++ b/roles/cloud-azure/tasks/prompts.yml @@ -0,0 +1,70 @@ +--- +- pause: + prompt: | + Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_secret + when: + - azure_secret is undefined + - lookup('env','AZURE_SECRET')|length <= 0 + +- pause: + prompt: | + Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_tenant + when: + - azure_tenant is undefined + - lookup('env','AZURE_TENANT')|length <= 0 + +- pause: + prompt: | + Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_client_id + when: + - azure_client_id is undefined + - lookup('env','AZURE_CLIENT_ID')|length <= 0 + +- pause: + prompt: | + Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) + You can skip this step if you want to use your defaults credentials from ~/.azure/credentials + echo: false + register: _azure_subscription_id + when: + - azure_subscription_id is undefined + - lookup('env','AZURE_SUBSCRIPTION_ID')|length <= 0 + +- set_fact: + secret: "{{ azure_secret | default(_azure_secret.user_input|default(None)) | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(_azure_tenant.user_input|default(None)) | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(_azure_client_id.user_input|default(None)) | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(_azure_subscription_id.user_input|default(None)) | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + +- block: + - name: Set facts about the regions + set_fact: + aws_regions: "{{ azure_regions | sort(attribute='region_name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in aws_regions %} + {%- if r['region_name'] == "us-east-1" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + {% for r in aws_regions %} + {{ loop.index }}. {{ r['region_name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-digitalocean/handlers/main.yml b/roles/cloud-digitalocean/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index f4932998..aca66b7b 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,7 +1,13 @@ - block: - - name: Set the DigitalOcean Access Token fact + - name: Include prompts + import_tasks: prompts.yml + + - name: Set additional facts set_fact: - do_token: "{{ do_access_token | default(lookup('env','DO_API_TOKEN'), true) }}" + algo_do_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ do_regions[_algo_region.user_input | int -1 ]['slug'] }} + {%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - block: @@ -9,7 +15,7 @@ digital_ocean: state: absent command: ssh - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys until: ssh_keys.changed != true @@ -21,7 +27,7 @@ digital_ocean: state: absent command: ssh - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys ignore_errors: yes @@ -36,7 +42,7 @@ state: present command: ssh ssh_pub_key: "{{ public_key }}" - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: do_ssh_key @@ -44,69 +50,33 @@ digital_ocean: state: present command: droplet - name: "{{ do_server_name }}" - region_id: "{{ do_region }}" + name: "{{ algo_server_name }}" + region_id: "{{ algo_do_region }}" size_id: "{{ cloud_providers.digitalocean.size }}" image_id: "{{ cloud_providers.digitalocean.image }}" ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" unique_name: yes - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" ipv6: yes register: do - - name: Add the droplet to an inventory group - add_host: - name: "{{ do.droplet.ip_address }}" - groups: vpn-host - ansible_ssh_user: root - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - do_access_token: "{{ do_token }}" - do_droplet_id: "{{ do.droplet.id }}" - cloud_provider: digitalocean - - set_fact: cloud_instance_ip: "{{ do.droplet.ip_address }}" + ansible_ssh_user: root - name: Tag the droplet digital_ocean_tag: name: "Environment:Algo" resource_id: "{{ do.droplet.id }}" - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" state: present - - name: Get droplets - uri: - url: "https://api.digitalocean.com/v2/droplets?tag_name=Environment:Algo" - method: GET - status_code: 200 - headers: - Content-Type: "application/json" - Authorization: "Bearer {{ do_token }}" - register: do_droplets - - - name: Ensure the group digitalocean exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[digitalocean]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[digitalocean\]' - regexp: "^{{ item.networks.v4[0].ip_address }}.*" - line: "{{ item.networks.v4[0].ip_address }}" - with_items: - - "{{ do_droplets.json.droplets }}" - - block: - name: "Delete the new Algo SSH key" digital_ocean: state: absent command: ssh - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys until: ssh_keys.changed != true @@ -118,7 +88,7 @@ digital_ocean: state: absent command: ssh - api_token: "{{ do_token }}" + api_token: "{{ algo_do_token }}" name: "{{ SSH_keys.comment }}" register: ssh_keys ignore_errors: yes diff --git a/roles/cloud-digitalocean/tasks/prompts.yml b/roles/cloud-digitalocean/tasks/prompts.yml new file mode 100644 index 00000000..f2804ca8 --- /dev/null +++ b/roles/cloud-digitalocean/tasks/prompts.yml @@ -0,0 +1,46 @@ +--- +- pause: + prompt: | + Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): + echo: false + register: _do_token + when: + - do_token is undefined + - lookup('env','DO_API_TOKEN')|length <= 0 + +- name: Set the token as a fact + set_fact: + algo_do_token: "{{ do_token | default(_do_token.user_input|default(None)) | default(lookup('env','DO_API_TOKEN'), true) }}" + +- name: Get regions + uri: + url: https://api.digitalocean.com/v2/regions + method: GET + status_code: 200 + headers: + Content-Type: "application/json" + Authorization: "Bearer {{ algo_do_token }}" + register: _do_regions + +- name: Set facts about thre regions + set_fact: + do_regions: "{{ _do_regions.json.regions | sort(attribute='slug') }}" + +- name: Set default region + set_fact: + default_region: >- + {% for r in do_regions %} + {%- if r['slug'] == "nyc3" %}{{ loop.index }}{% endif %} + {%- endfor %} + +- pause: + prompt: | + What region should the server be located in? + {% for r in do_regions %} + {{ loop.index }}. {{ r['slug'] }} {{ r['name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 b/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 deleted file mode 100644 index 7db27bbb..00000000 --- a/roles/cloud-digitalocean/templates/20-ipv6.cfg.j2 +++ /dev/null @@ -1,6 +0,0 @@ -iface eth0 inet6 static - address {{ item.ip_address }} - netmask {{ item.netmask }} - gateway {{ item.gateway }} - autoconf 0 - dns-nameservers 2001:4860:4860::8844 2001:4860:4860::8888 diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 045fe455..8060eb72 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -1,5 +1,6 @@ --- - +ami_search_encrypted: omit +encrypted: "{{ cloud_providers.ec2.encrypted }}" ec2_vpc_nets: cidr_block: 172.16.0.0/16 subnet_cidr: 172.16.254.0/23 diff --git a/roles/cloud-ec2/handlers/main.yml b/roles/cloud-ec2/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-ec2/tasks/cloudformation.yml b/roles/cloud-ec2/tasks/cloudformation.yml index 7c6fe374..27977203 100644 --- a/roles/cloud-ec2/tasks/cloudformation.yml +++ b/roles/cloud-ec2/tasks/cloudformation.yml @@ -1,11 +1,11 @@ --- - name: Deploy the template cloudformation: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" stack_name: "{{ stack_name }}" state: "present" - region: "{{ region }}" + region: "{{ algo_region }}" template: roles/cloud-ec2/files/stack.yml template_parameters: InstanceTypeParameter: "{{ cloud_providers.ec2.size }}" diff --git a/roles/cloud-ec2/tasks/encrypt_image.yml b/roles/cloud-ec2/tasks/encrypt_image.yml index 11779ea4..967e274d 100644 --- a/roles/cloud-ec2/tasks/encrypt_image.yml +++ b/roles/cloud-ec2/tasks/encrypt_image.yml @@ -1,37 +1,27 @@ +--- - name: Check if the encrypted image already exist - ec2_ami_find: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" - owner: self - sort: creationDate - sort_order: descending - sort_end: 1 - state: available - ami_tags: - Algo: "encrypted" - region: "{{ region }}" + ec2_ami_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + owners: self + region: "{{ algo_region }}" + filters: + state: available + "tag:Algo": encrypted register: search_crypt -- set_fact: - ami_image: "{{ search_crypt.results[0].ami_id }}" - when: search_crypt.results - - name: Copy to an encrypted image ec2_ami_copy: - aws_access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true)}}" - aws_secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true)}}" + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" encrypted: yes name: algo kms_key_id: "{{ kms_key_id | default(omit) }}" - region: "{{ region }}" - source_image_id: "{{ ami_image }}" - source_region: "{{ region }}" + region: "{{ algo_region }}" + source_image_id: "{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}" + source_region: "{{ algo_region }}" + wait: true tags: Algo: "encrypted" - wait: true - register: enc_image - when: not search_crypt.results - -- set_fact: - ami_image: "{{ enc_image.image_id }}" - when: not search_crypt.results + register: ami_search_encrypted + when: search_crypt.images|length|int == 0 diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 0e820b84..64dbfcd4 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,66 +1,40 @@ - block: + - name: Include prompts + import_tasks: prompts.yml + - set_fact: - access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" - secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" - stack_name: "{{ aws_server_name | replace('.', '-') }}" + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ aws_regions[_algo_region.user_input | int -1 ]['region_name'] }} + {%- else %}{{ aws_regions[default_region | int - 1]['region_name'] }}{% endif %} + stack_name: "{{ algo_server_name | replace('.', '-') }}" - name: Locate official AMI for region - ec2_ami_find: + ec2_ami_facts: aws_access_key: "{{ access_key }}" aws_secret_key: "{{ secret_key }}" - name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" - owner: "{{ cloud_providers.ec2.image.owner }}" - sort: creationDate - sort_order: descending - sort_end: 1 - region: "{{ region }}" + owners: "{{ cloud_providers.ec2.image.owner }}" + region: "{{ algo_region }}" + filters: + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" register: ami_search - - set_fact: - ami_image: "{{ ami_search.results[0].ami_id }}" + - import_tasks: encrypt_image.yml + when: encrypted - - include_tasks: encrypt_image.yml - tags: [encrypted] + - name: Set the ami id as a fact + set_fact: + ami_image: >- + {% if ami_search_encrypted.image_id is defined %}{{ ami_search_encrypted.image_id }} + {%- elif search_crypt.images is defined and search_crypt.images|length >= 1 %}{{ (search_crypt.images | sort(attribute='creation_date') | last)['image_id'] }} + {%- else %}{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}{% endif %} - - include_tasks: cloudformation.yml - - - name: Add new instance to host group - add_host: - hostname: "{{ stack.stack_outputs.ElasticIP }}" - groupname: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: ec2 + - name: Deploy the stack + import_tasks: cloudformation.yml - set_fact: cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" - - - name: Get EC2 instances - ec2_instance_facts: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - region: "{{ region }}" - filters: - instance-state-name: running - "tag:Environment": Algo - register: algo_instances - - - name: Ensure the group ec2 exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[ec2]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[ec2\]' - regexp: "^{{ item.public_ip_address }}.*" - line: "{{ item.public_ip_address }}" - with_items: - - "{{ algo_instances.instances }}" + ansible_ssh_user: ubuntu rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-ec2/tasks/prompts.yml b/roles/cloud-ec2/tasks/prompts.yml new file mode 100644 index 00000000..2993f694 --- /dev/null +++ b/roles/cloud-ec2/tasks/prompts.yml @@ -0,0 +1,55 @@ +--- +- pause: + prompt: | + Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md) + echo: false + register: _aws_access_key + when: + - aws_access_key is undefined + - lookup('env','AWS_ACCESS_KEY_ID')|length <= 0 + +- pause: + prompt: | + Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + echo: false + register: _aws_secret_key + when: + - aws_secret_key is undefined + - lookup('env','AWS_SECRET_ACCESS_KEY')|length <= 0 + +- set_fact: + access_key: "{{ aws_access_key | default(_aws_access_key.user_input|default(None)) | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(_aws_secret_key.user_input|default(None)) | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + +- block: + - name: Get regions + aws_region_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + region: us-east-1 + register: _aws_regions + + - name: Set facts about the regions + set_fact: + aws_regions: "{{ _aws_regions.regions | sort(attribute='region_name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in aws_regions %} + {%- if r['region_name'] == "us-east-1" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) + {% for r in aws_regions %} + {{ loop.index }}. {{ r['region_name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined diff --git a/roles/cloud-gce/handlers/main.yml b/roles/cloud-gce/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 24a825cf..8dad0a08 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,68 +1,37 @@ - block: - - set_fact: - credentials_file_path: "{{ credentials_file | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" - ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - - - set_fact: - credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" - - - set_fact: - service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" - project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" - server_name: "{{ gce_server_name | replace('_', '-') }}" + - name: Include prompts + import_tasks: prompts.yml - name: Network configured gce_net: - name: "algo-net-{{ server_name }}" - fwname: "algo-net-{{ server_name }}-fw" + name: "algo-net-{{ algo_server_name }}" + fwname: "algo-net-{{ algo_server_name }}-fw" allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" state: "present" mode: auto src_range: 0.0.0.0/0 - service_account_email: "{{ credentials_file_lookup.client_email }}" - credentials_file: "{{ credentials_file }}" - project_id: "{{ credentials_file_lookup.project_id }}" + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" - name: "Creating a new instance..." gce: - instance_names: "{{ server_name }}" - zone: "{{ zone }}" + instance_names: "{{ algo_server_name }}" + zone: "{{ algo_region }}" machine_type: "{{ cloud_providers.gce.size }}" image: "{{ cloud_providers.gce.image }}" service_account_email: "{{ service_account_email }}" credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ server_name }}" + network: "algo-net-{{ algo_server_name }}" tags: - "environment-algo" register: google_vm - - name: Add the instance to an inventory group - add_host: - name: "{{ google_vm.instance_data[0].public_ip }}" - groups: vpn-host - ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: gce - - set_fact: cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" - - - name: Ensure the group gce exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[gce]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[gce\]' - regexp: "^{{ google_vm.instance_data[0].public_ip }}.*" - line: "{{ google_vm.instance_data[0].public_ip }}" + ansible_ssh_user: ubuntu rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-gce/tasks/prompts.yml b/roles/cloud-gce/tasks/prompts.yml new file mode 100644 index 00000000..b054cc9e --- /dev/null +++ b/roles/cloud-gce/tasks/prompts.yml @@ -0,0 +1,67 @@ +--- +- pause: + prompt: | + Enter the local path to your credentials JSON file + (https://support.google.com/cloud/answer/6158849?hl=en&ref_topic=6262490#serviceaccounts) + register: _gce_credentials_file + when: + - gce_credentials_file is undefined + - lookup('env','GCE_CREDENTIALS_FILE_PATH')|length <= 0 + +- set_fact: + credentials_file_path: "{{ gce_credentials_file | default(_gce_credentials_file.user_input|default(None)) | default(lookup('env','GCE_CREDENTIALS_FILE_PATH'), true) }}" + ssh_public_key_lookup: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + +- set_fact: + credentials_file_lookup: "{{ lookup('file', '{{ credentials_file_path }}') }}" + +- set_fact: + service_account_email: "{{ credentials_file_lookup.client_email | default(lookup('env','GCE_EMAIL')) }}" + project_id: "{{ credentials_file_lookup.project_id | default(lookup('env','GCE_PROJECT')) }}" + +- block: + - name: Get regions + gce_region_facts: + service_account_email: "{{ credentials_file_lookup.client_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ credentials_file_lookup.project_id }}" + register: _gce_regions + + - name: Set facts about the regions + set_fact: + gce_regions: >- + [{%- for region in _gce_regions.results.regions | sort(attribute='name') -%} + {% if region.status == "UP" %} + {% for zone in region.zones | sort(attribute='name') %} + {% if zone.status == "UP" %} + '{{ zone.name }}' + {% endif %}{% if not loop.last %},{% endif %} + {% endfor %} + {% endif %}{% if not loop.last %},{% endif %} + {%- endfor -%}] + + - name: Set facts about the default region + set_fact: + default_region: >- + {% for region in gce_regions %} + {%- if region == "us-east1-b" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://cloud.google.com/compute/docs/regions-zones/) + {% for r in gce_regions %} + {{ loop.index }}. {{ r }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _gce_region + when: region is undefined + +- set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _gce_region.user_input is defined and _gce_region.user_input != "" %}{{ gce_regions[_gce_region.user_input | int -1 ] }} + {%- else %}{{ gce_regions[default_region | int - 1] }}{% endif %} diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index 31f73e6f..29342af9 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -1,8 +1,6 @@ - block: - - set_fact: - access_key: "{{ aws_access_key | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" - secret_key: "{{ aws_secret_key | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" - region: "{{ algo_region | default(lookup('env','AWS_DEFAULT_REGION'), true) }}" + - name: Include prompts + import_tasks: prompts.yml - name: Create an instance lightsail: @@ -10,8 +8,8 @@ aws_secret_key: "{{ secret_key }}" name: "{{ algo_server_name }}" state: present - region: "{{ region }}" - zone: "{{ region }}a" + region: "{{ algo_region }}" + zone: "{{ algo_region }}a" blueprint_id: "{{ cloud_providers.lightsail.image }}" bundle_id: "{{ cloud_providers.lightsail.size }}" wait_timeout: 300 @@ -37,15 +35,7 @@ - set_fact: cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" - - - name: Add new instance to host group - add_host: - hostname: "{{ cloud_instance_ip }}" - groupname: vpn-host ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: lightsail rescue: - debug: var=fail_hint diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml new file mode 100644 index 00000000..26d50a57 --- /dev/null +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -0,0 +1,60 @@ +--- +- pause: + prompt: | + Enter your aws_access_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + Note: Make sure to use an IAM user with an acceptable policy attached (see https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md) + echo: false + register: _aws_access_key + when: + - aws_access_key is undefined + - lookup('env','AWS_ACCESS_KEY_ID')|length <= 0 + +- pause: + prompt: | + Enter your aws_secret_key (http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html) + echo: false + register: _aws_secret_key + when: + - aws_secret_key is undefined + - lookup('env','AWS_SECRET_ACCESS_KEY')|length <= 0 + +- set_fact: + access_key: "{{ aws_access_key | default(_aws_access_key.user_input|default(None)) | default(lookup('env','AWS_ACCESS_KEY_ID'), true) }}" + secret_key: "{{ aws_secret_key | default(_aws_secret_key.user_input|default(None)) | default(lookup('env','AWS_SECRET_ACCESS_KEY'), true) }}" + +- block: + - name: Get regions + lightsail_region_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + register: _lightsail_regions + + - name: Set facts about thre regions + set_fact: + lightsail_regions: "{{ _lightsail_regions.results.regions | sort(attribute='name') }}" + + - name: Set the default region + set_fact: + default_region: >- + {% for r in lightsail_regions %} + {%- if r['name'] == "eu-west-1" %}{{ loop.index }}{% endif %} + {%- endfor %} + + - pause: + prompt: | + What region should the server be located in? + (https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) + {% for r in lightsail_regions %} + {{ loop.index }}. {{ r['name'] }} {{ r['displayName'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined + +- set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ lightsail_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ lightsail_regions[default_region | int - 1]['name'] }}{% endif %} diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index d470e89e..8fb1e6b0 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -1,4 +1,8 @@ --- +- fail: + msg: "OpenStack credentials are not set. Download it from the OpenStack dashboard->Compute->API Access and source it in the shell (eg: source /tmp/dhc-openrc.sh)" + when: lookup('env', 'OS_AUTH_URL') == "" + - block: - name: Security group created os_security_group: @@ -70,15 +74,7 @@ - set_fact: cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" - - - name: Add new instance to host group - add_host: - hostname: "{{ cloud_instance_ip }}" - groupname: vpn-host ansible_ssh_user: ubuntu - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: openstack rescue: - debug: var=fail_hint diff --git a/roles/cloud-scaleway/defaults/main.yml b/roles/cloud-scaleway/defaults/main.yml new file mode 100644 index 00000000..00c1dc10 --- /dev/null +++ b/roles/cloud-scaleway/defaults/main.yml @@ -0,0 +1,4 @@ +--- +scaleway_regions: + - alias: par1 + - alias: ams1 diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 1bc939b8..9242fb3a 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -1,11 +1,14 @@ - block: + - name: Include prompts + import_tasks: prompts.yml + - name: Check if server exists uri: url: "https://cp-{{ algo_region }}.scaleway.com/servers" method: GET headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" status_code: 200 register: scaleway_servers @@ -24,7 +27,7 @@ method: GET headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" status_code: 200 register: scaleway_organizations @@ -32,7 +35,7 @@ set_fact: organization_id: "{{ item.id }}" no_log: true - when: scaleway_organization == item.name + when: algo_scaleway_org == item.name with_items: "{{ scaleway_organizations.json.organizations }}" - name: Get total count of images @@ -41,7 +44,7 @@ method: GET headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" status_code: 200 register: scaleway_pages @@ -68,7 +71,7 @@ method: POST headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" body: organization: "{{ organization_id }}" name: "{{ algo_server_name }}" @@ -94,7 +97,7 @@ method: POST headers: Content-Type: application/json - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" body: action: poweron status_code: 202 @@ -108,7 +111,7 @@ method: GET headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" status_code: 200 until: - algo_instance.json.server.state is defined @@ -119,15 +122,7 @@ - set_fact: cloud_instance_ip: "{{ algo_instance['json']['server']['public_ip']['address'] }}" - - - name: Add new instance to host group - add_host: - hostname: "{{ cloud_instance_ip }}" - groupname: vpn-host ansible_ssh_user: root - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - cloud_provider: scaleway rescue: - debug: var=fail_hint diff --git a/roles/cloud-scaleway/tasks/prompts.yml b/roles/cloud-scaleway/tasks/prompts.yml new file mode 100644 index 00000000..22c3f1aa --- /dev/null +++ b/roles/cloud-scaleway/tasks/prompts.yml @@ -0,0 +1,34 @@ +--- +- pause: + prompt: | + Enter your auth token (https://www.scaleway.com/docs/generate-an-api-token/) + echo: false + register: _scaleway_token + when: scaleway_token is undefined + +- pause: + prompt: | + Enter your organization name (https://cloud.scaleway.com/#/billing) + register: _scaleway_org + when: scaleway_org is undefined + +- pause: + prompt: | + What region should the server be located in? + {% for r in scaleway_regions %} + {{ loop.index }}. {{ r['alias'] }} + {% endfor %} + + Enter the number of your desired region + [{{ scaleway_regions.0.alias }}] + register: _algo_region + when: region is undefined + +- name: Set scaleway facts + set_fact: + algo_scaleway_token: "{{ scaleway_token | default(_scaleway_token.user_input) }}" + algo_scaleway_org: "{{ scaleway_org | default(_scaleway_org.user_input|default(omit)) }}" + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ scaleway_regions[_algo_region.user_input | int -1 ]['alias'] }} + {%- else %}{{ scaleway_regions.0.alias }}{% endif %} diff --git a/roles/cloud-vultr/tasks/main.yml b/roles/cloud-vultr/tasks/main.yml new file mode 100644 index 00000000..78e514d0 --- /dev/null +++ b/roles/cloud-vultr/tasks/main.yml @@ -0,0 +1,36 @@ +- block: + - name: Include prompts + import_tasks: prompts.yml + + - name: Upload the SSH key + vr_ssh_key: + name: "{{ SSH_keys.comment }}" + ssh_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + register: ssh_key + + - name: Creating a server + vr_server: + name: "{{ algo_server_name }}" + hostname: "{{ algo_server_name }}" + os: "{{ cloud_providers.vultr.os }}" + plan: "{{ cloud_providers.vultr.size }}" + region: "{{ algo_vultr_region }}" + state: started + tag: Environment:Algo + ssh_key: "{{ ssh_key.vultr_ssh_key.name }}" + ipv6_enabled: true + auto_backup_enabled: false + notify_activate: false + register: vultr_server + + - set_fact: + cloud_instance_ip: "{{ vultr_server.vultr_server.v4_main_ip }}" + ansible_ssh_user: root + + environment: + VULTR_API_CONFIG: "{{ algo_vultr_config }}" + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml new file mode 100644 index 00000000..84e0cfd9 --- /dev/null +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -0,0 +1,56 @@ +--- +- pause: + prompt: | + Enter the local path to your configuration INI file + (https://github.com/trailofbits/algo/docs/cloud-vultr.md): + register: _vultr_config + when: vultr_config is undefined + +- name: Set the token as a fact + set_fact: + algo_vultr_config: "{{ vultr_config | default(_vultr_config.user_input) | default(lookup('env','VULTR_API_CONFIG'), true) }}" + +- name: Get regions + uri: + url: https://api.vultr.com/v1/regions/list + method: GET + status_code: 200 + register: _vultr_regions + +- name: Format regions + set_fact: + regions: >- + [ {% for k, v in _vultr_regions.json.items() %} + {{ v }}{% if not loop.last %},{% endif %} + {% endfor %} ] + +- name: Set regions as a fact + set_fact: + vultr_regions: "{{ regions | sort(attribute='country') }}" + +- name: Set default region + set_fact: + default_region: >- + {% for r in vultr_regions %} + {%- if r['DCID'] == "1" %}{{ loop.index }}{% endif %} + {%- endfor %} + +- pause: + prompt: | + What region should the server be located in? + (https://www.vultr.com/locations/): + {% for r in vultr_regions %} + {{ loop.index }}. {{ r['name'] }} + {% endfor %} + + Enter the number of your desired region + [{{ default_region }}] + register: _algo_region + when: region is undefined + +- name: Set the desired region as a fact + set_fact: + algo_vultr_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ vultr_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ vultr_regions[default_region | int - 1]['name'] }}{% endif %} diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml new file mode 100644 index 00000000..8182cf20 --- /dev/null +++ b/roles/common/tasks/facts.yml @@ -0,0 +1,26 @@ +--- +- block: + - name: Generate password for the CA key + local_action: + module: shell + openssl rand -hex 16 + register: CA_password + + - name: Generate p12 export password + local_action: + module: shell + openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' + register: p12_password_generated + when: p12_password is not defined + tags: update-users + become: false + +- name: Define facts + set_fact: + p12_export_password: "{{ p12_password|default(p12_password_generated.stdout) }}" + tags: update-users + +- set_fact: + CA_password: "{{ CA_password.stdout }}" + IP_subject_alt_name: "{{ IP_subject_alt_name }}" + ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 67d247d8..dc52931c 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,6 +1,13 @@ --- - - set_fact: + config_prefix: "/usr/local/" + root_group: wheel + ssh_service_name: sshd + apparmor_enabled: false + strongswan_additional_plugins: + - kernel-pfroute + - kernel-pfkey + ansible_python_interpreter: /usr/local/bin/python2.7 tools: - git - subversion @@ -17,6 +24,15 @@ tags: - always +- setup: + +- name: Install tools + package: name="{{ item }}" state=present + with_items: + - "{{ tools|default([]) }}" + tags: + - always + - name: Loopback included into the rc config blockinfile: dest: /etc/rc.conf diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 5b6aa438..73e6783f 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -1,26 +1,26 @@ --- - block: - - include_tasks: ubuntu.yml - when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - name: Check the system + raw: uname -a + register: OS - - include_tasks: freebsd.yml - when: ansible_distribution == 'FreeBSD' + - include_tasks: ubuntu.yml + when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' - - name: Install tools - package: name="{{ item }}" state=present - with_items: - - "{{ tools|default([]) }}" - tags: - - always + - include_tasks: freebsd.yml + when: '"FreeBSD" in OS.stdout' - - name: Sysctl tuning - sysctl: name="{{ item.item }}" value="{{ item.value }}" - with_items: - - "{{ sysctl|default([]) }}" - tags: - - always + - name: Gather additional facts + import_tasks: facts.yml - - meta: flush_handlers + - name: Sysctl tuning + sysctl: name="{{ item.item }}" value="{{ item.value }}" + with_items: + - "{{ sysctl|default([]) }}" + tags: + - always + + - meta: flush_handlers rescue: - debug: var=fail_hint tags: always diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index f2799ab0..fee3af42 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -1,52 +1,69 @@ --- +- block: + - name: Ubuntu | Install prerequisites + apt: + name: "{{ item }}" + update_cache: true + with_items: + - python2.7 + - sudo + + - name: Ubuntu | Configure defaults + alternatives: + name: python + link: /usr/bin/python + path: /usr/bin/python2.7 + priority: 1 + tags: + - update-alternatives + vars: + ansible_python_interpreter: /usr/bin/python3 + +- name: Gather facts + setup: + - name: Cloud only tasks block: - - name: Install software updates - apt: - update_cache: true - install_recommends: true - upgrade: dist + - name: Install software updates + apt: + update_cache: true + install_recommends: true + upgrade: dist - - name: Upgrade the ca certificates - apt: - name: ca-certificates - state: latest + - name: Check if reboot is required + shell: > + if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi + args: + executable: /bin/bash + register: reboot_required - - name: Check if reboot is required - shell: > - if [[ -e /var/run/reboot-required ]]; then echo "required"; else echo "no"; fi - args: - executable: /bin/bash - register: reboot_required + - name: Reboot + shell: sleep 2 && shutdown -r now "Ansible updates triggered" + async: 1 + poll: 0 + when: reboot_required is defined and reboot_required.stdout == 'required' + ignore_errors: true - - name: Reboot - shell: sleep 2 && shutdown -r now "Ansible updates triggered" - async: 1 - poll: 0 - when: reboot_required is defined and reboot_required.stdout == 'required' - ignore_errors: true + - name: Wait until SSH becomes ready... + local_action: + module: wait_for + port: 22 + host: "{{ inventory_hostname }}" + search_regex: OpenSSH + delay: 10 + timeout: 320 + when: reboot_required is defined and reboot_required.stdout == 'required' + become: false + when: algo_provider != "local" - - name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ inventory_hostname }}" - search_regex: OpenSSH - delay: 10 - timeout: 320 - when: reboot_required is defined and reboot_required.stdout == 'required' - become: false +- name: Include unatteded upgrades configuration + import_tasks: unattended-upgrades.yml - - name: Include unatteded upgrades configuration - include_tasks: unattended-upgrades.yml - - - name: Disable MOTD on login and SSHD - replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" - with_items: - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } - - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } - tags: - - cloud +- name: Disable MOTD on login and SSHD + replace: dest="{{ item.file }}" regexp="{{ item.regexp }}" replace="{{ item.line }}" + with_items: + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/login' } + - { regexp: '^session.*optional.*pam_motd.so.*', line: '# MOTD DISABLED', file: '/etc/pam.d/sshd' } - name: Loopback for services configured template: @@ -101,3 +118,10 @@ value: 1 tags: - always + +- name: Install tools + package: name="{{ item }}" state=present + with_items: + - "{{ tools|default([]) }}" + tags: + - always diff --git a/roles/dns_adblocking/meta/main.yml b/roles/dns_adblocking/meta/main.yml deleted file mode 100644 index 5543bcab..00000000 --- a/roles/dns_adblocking/meta/main.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } - - role: dns_encryption - tags: dns_encryption - when: dns_encryption == true diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index a68abeed..b276d355 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -1,10 +1,5 @@ --- - block: - - - name: The DNS tag is defined - set_fact: - local_dns: true - - name: Dnsmasq installed package: name=dnsmasq diff --git a/roles/dns_adblocking/templates/dnsmasq.conf.j2 b/roles/dns_adblocking/templates/dnsmasq.conf.j2 index 0e6e72f5..c52b6b9c 100644 --- a/roles/dns_adblocking/templates/dnsmasq.conf.j2 +++ b/roles/dns_adblocking/templates/dnsmasq.conf.j2 @@ -88,7 +88,7 @@ no-resolv # You can control how dnsmasq talks to a server: this forces # queries to 10.1.2.3 to be routed via eth1 # server=10.1.2.3@eth1 -{% if dns_encryption|default(false)|bool == true %} +{% if dns_encryption %} server={{ local_service_ip }}#5353 {% else %} {% for host in dns_servers.ipv4 %} diff --git a/roles/dns_encryption/defaults/main.yml b/roles/dns_encryption/defaults/main.yml index df031a90..5997f58a 100644 --- a/roles/dns_encryption/defaults/main.yml +++ b/roles/dns_encryption/defaults/main.yml @@ -1,7 +1,9 @@ --- -listen_port: "{% if local_dns|d(false)|bool == true %}5353{% else %}53{% endif %}" +algo_local_dns: false +listen_port: "{% if algo_local_dns %}5353{% else %}53{% endif %}" # the version used if the latest unavailable (in case of Github API rate limited) dnscrypt_proxy_version: 2.0.10 apparmor_enabled: true dns_encryption: true dns_encryption_provider: "*" +ipv6_support: false diff --git a/roles/dns_encryption/handlers/main.yml b/roles/dns_encryption/handlers/main.yml index 7947ef11..fe677147 100644 --- a/roles/dns_encryption/handlers/main.yml +++ b/roles/dns_encryption/handlers/main.yml @@ -8,3 +8,10 @@ name: dnscrypt-proxy state: restarted daemon_reload: true + when: ansible_distribution == 'Ubuntu' + +- name: restart dnscrypt-proxy + service: + name: dnscrypt-proxy + state: restarted + when: ansible_distribution == 'FreeBSD' diff --git a/roles/dns_encryption/meta/main.yml b/roles/dns_encryption/meta/main.yml deleted file mode 100644 index 9119c109..00000000 --- a/roles/dns_encryption/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -dependencies: - - role: common - tags: common diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index f42d0a90..13ba1709 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -5,7 +5,7 @@ codename: bionic repo: ppa:shevchuk/dnscrypt-proxy register: result - until: result|succeeded + until: result is succeeded retries: 10 delay: 3 diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index f99aeda0..18a8bebb 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -134,7 +134,7 @@ tls_disable_session_tickets = true ## Keep tls_cipher_suite empty if you have issues fetching sources or ## connecting to some DoH servers. Google and Cloudflare are fine with it. -tls_cipher_suite = [49195] +# tls_cipher_suite = [49195] ## Fallback resolver diff --git a/roles/local/handlers/main.yml b/roles/local/handlers/main.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/roles/local/tasks/main.yml b/roles/local/tasks/main.yml index 555baa45..5803cff9 100644 --- a/roles/local/tasks/main.yml +++ b/roles/local/tasks/main.yml @@ -1,40 +1,8 @@ --- - block: - - name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - cloud_provider: local - when: server_ip != "localhost" + - name: Include prompts + import_tasks: prompts.yml - - name: Add the instance to an inventory group - add_host: - name: "{{ server_ip }}" - groups: vpn-host - ansible_ssh_user: "{{ server_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" - ansible_connection: local - cloud_provider: local - when: server_ip == "localhost" - - - set_fact: - cloud_instance_ip: "{{ server_ip }}" - - - name: Ensure the group local exists in the dynamic inventory file - lineinfile: - state: present - dest: configs/inventory.dynamic - line: '[local]' - - - name: Populate the dynamic inventory - lineinfile: - state: present - dest: configs/inventory.dynamic - insertafter: '\[local\]' - regexp: "^{{ server_ip }}.*" - line: "{{ server_ip }}" rescue: - debug: var=fail_hint tags: always diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml new file mode 100644 index 00000000..1f5edc2e --- /dev/null +++ b/roles/local/tasks/prompts.yml @@ -0,0 +1,44 @@ +--- +- pause: + prompt: | + Enter the IP address of your server: (or use localhost for local installation): + [localhost] + register: _algo_server + when: server is undefined + +- name: Set the facts + set_fact: + cloud_instance_ip: >- + {% if server is defined %}{{ server }} + {%- elif _algo_server.user_input is defined and _algo_server.user_input != "" %}{{ _algo_server.user_input }} + {%- else %}localhost{% endif %} + +- pause: + prompt: | + What user should we use to login on the server? (note: passwordless login required, or ignore if you're deploying to localhost) + [root] + register: _algo_ssh_user + when: + - ssh_user is undefined + - cloud_instance_ip != "localhost" + +- name: Set the facts + set_fact: + ansible_ssh_user: >- + {% if ssh_user is defined %}{{ ssh_user }} + {%- elif _algo_ssh_user.user_input is defined and _algo_ssh_user.user_input != "" %}{{ _algo_ssh_user.user_input }} + {%- else %}root{% endif %} + +- pause: + prompt: | + Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) + [{{ cloud_instance_ip }}] + register: _endpoint + when: endpoint is undefined + +- name: Set the facts + set_fact: + IP_subject_alt_name: >- + {% if endpoint is defined %}{{ endpoint }} + {%- elif _endpoint.user_input is defined and _endpoint.user_input != "" %}{{ _endpoint.user_input }} + {%- else %}{{ cloud_instance_ip }}{% endif %} diff --git a/roles/ssh_tunneling/meta/main.yml b/roles/ssh_tunneling/meta/main.yml deleted file mode 100644 index e985f927..00000000 --- a/roles/ssh_tunneling/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -dependencies: - - { role: common, tags: common } diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 8a1d4965..860a329d 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -36,11 +36,12 @@ ssh_key_type: ecdsa ssh_key_bits: 256 ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - ssh_key_passphrase: "{{ easyrsa_p12_export_password }}" + ssh_key_passphrase: "{{ p12_export_password }}" update_password: on_create state: present append: yes with_items: "{{ users }}" + tags: update-users - name: The authorized keys file created file: @@ -50,6 +51,7 @@ group: "{{ item }}" state: link with_items: "{{ users }}" + tags: update-users - name: Generate SSH fingerprints shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null @@ -60,12 +62,9 @@ src: '/var/jail/{{ item }}/.ssh/id_ecdsa' dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem flat: yes + mode: "0600" with_items: "{{ users }}" - - - name: Change mode for SSH private keys - local_action: file path=configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem mode=0600 - with_items: "{{ users }}" - become: false + tags: update-users - name: Fetch the known_hosts file local_action: @@ -80,15 +79,15 @@ src: ssh_config.j2 dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config mode: 0600 - become: no - with_items: - - "{{ users }}" + become: false + tags: update-users + with_items: "{{ users }}" - name: SSH | Get active system users shell: > getent group algo | cut -f4 -d: | sed "s/,/\n/g" register: valid_users - when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + tags: update-users - name: SSH | Delete non-existing users user: @@ -96,8 +95,9 @@ state: absent remove: yes force: yes - when: item not in users and ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" + when: item not in users with_items: "{{ valid_users.stdout_lines | default('null') }}" + tags: update-users rescue: - debug: var=fail_hint tags: always diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index f969fb29..51b06bf8 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,5 +1,37 @@ --- +BetweenClients_DROP: true +wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" +wireguard_interface: wg0 +wireguard_network_ipv4: + subnet: 10.19.49.0 + prefix: 24 + gateway: 10.19.49.1 + clients_range: 10.19.49 + clients_start: 100 +wireguard_network_ipv6: + subnet: 'fd9d:bc11:4021::' + prefix: 48 + gateway: 'fd9d:bc11:4021::1' + clients_range: 'fd9d:bc11:4021::' + clients_start: 100 +wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" +wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" +keys_clean_all: false +wireguard_dns_servers: >- + {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} + {{ local_service_ip }} + {% else %} + {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + {% endif %} + +algo_ondemand_cellular: false +algo_ondemand_wifi: false +algo_ondemand_wifi_exclude: '_null' +algo_windows: false +algo_store_cakey: false +algo_local_dns: false ipv6_support: false +dns_encryption: true domain: false subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" openssl_bin: openssl diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index 5543bcab..5f86e875 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -1,7 +1,6 @@ --- dependencies: - - { role: common, tags: common } - role: dns_encryption tags: dns_encryption - when: dns_encryption == true + when: dns_encryption diff --git a/roles/vpn/tasks/client_configs.yml b/roles/vpn/tasks/client_configs.yml index 52dff83c..827bef76 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/vpn/tasks/client_configs.yml @@ -37,23 +37,12 @@ with_items: - "{{ users }}" -- name: Create the windows check file - file: - state: touch - path: configs/{{ IP_subject_alt_name }}/.supports_windows - when: Win10_Enabled is defined and Win10_Enabled == "Y" - -- name: Check if the windows check file exists - stat: - path: configs/{{ IP_subject_alt_name }}/.supports_windows - register: supports_windows - - name: Build the windows client powershell script template: src: client_windows.ps1.j2 dest: configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 mode: 0600 - when: Win10_Enabled is defined and Win10_Enabled == "Y" or supports_windows.stat.exists == true + when: algo_windows with_together: - "{{ users }}" - "{{ PayloadContent.results }}" diff --git a/roles/vpn/tasks/freebsd.yml b/roles/vpn/tasks/freebsd.yml deleted file mode 100644 index 43cfbf63..00000000 --- a/roles/vpn/tasks/freebsd.yml +++ /dev/null @@ -1,114 +0,0 @@ ---- - -- name: FreeBSD / HardenedBSD | Get the existing kernel parameters - command: sysctl -b kern.conftxt - register: kern_conftxt - when: rebuild_kernel is defined and rebuild_kernel == "true" - -- name: FreeBSD / HardenedBSD | Set the rebuild_needed fact - set_fact: - rebuild_needed: true - when: item not in kern_conftxt.stdout and rebuild_kernel is defined and rebuild_kernel == "true" - with_items: - - "IPSEC" - - "IPSEC_NAT_T" - - "crypto" - -- name: FreeBSD / HardenedBSD | Make the kernel config - shell: sysctl -b kern.conftxt > /tmp/IPSEC - when: rebuild_needed is defined and rebuild_needed == true - -- name: FreeBSD / HardenedBSD | Ensure the all options are enabled - lineinfile: - dest: /tmp/IPSEC - line: "{{ item }}" - insertbefore: BOF - with_items: - - "options IPSEC" - - "options IPSEC_NAT_T" - - "device crypto" - when: rebuild_needed is defined and rebuild_needed == true - -- name: HardenedBSD | Determine the sources - set_fact: - sources_repo: https://github.com/HardenedBSD/hardenedBSD.git - sources_version: "hardened/{{ ansible_distribution_release.split('.')[0] }}-stable/master" - when: "'Hardened' in ansible_distribution_version" - -- name: FreeBSD | Determine the sources - set_fact: - sources_repo: https://github.com/freebsd/freebsd.git - sources_version: "stable/{{ ansible_distribution_major_version }}" - when: "'Hardened' not in ansible_distribution_version" - -- name: FreeBSD / HardenedBSD | Increase the git postBuffer size - git_config: - name: http.postBuffer - scope: global - value: 1048576000 - -- block: - - name: FreeBSD / HardenedBSD | Fetching the sources... - git: - repo: "{{ sources_repo }}" - dest: /usr/krnl_src - version: "{{ sources_version }}" - accept_hostkey: true - async: 1000 - poll: 0 - register: fetching_sources - - - name: FreeBSD / HardenedBSD | Fetching the sources... - async_status: jid={{ fetching_sources.ansible_job_id }} - when: rebuild_needed is defined and rebuild_needed == true - register: result - until: result.finished - retries: 600 - delay: 30 - rescue: - - debug: var=fetching_sources - - - fail: - msg: "Something went wrong. Check the debug output above." - -- block: - - name: FreeBSD / HardenedBSD | The kernel is being built... - shell: > - mv /tmp/IPSEC /usr/krnl_src/sys/{{ ansible_architecture }}/conf && - make buildkernel KERNCONF=IPSEC && - make installkernel KERNCONF=IPSEC - args: - chdir: /usr/krnl_src - executable: /usr/local/bin/bash - when: rebuild_needed is defined and rebuild_needed == true - async: 1000 - poll: 0 - register: building_kernel - - - name: FreeBSD / HardenedBSD | The kernel is being built... - async_status: jid={{ building_kernel.ansible_job_id }} - when: rebuild_needed is defined and rebuild_needed == true - register: result - until: result.finished - retries: 600 - delay: 30 - rescue: - - debug: var=building_kernel - - - fail: - msg: "Something went wrong. Check the debug output above." - -- name: FreeBSD / HardenedBSD | Reboot - shell: sleep 2 && shutdown -r now - args: - executable: /usr/local/bin/bash - when: rebuild_needed is defined and rebuild_needed == true - async: 1 - poll: 0 - ignore_errors: true - -- name: FreeBSD / HardenedBSD | Enable strongswan - lineinfile: - dest: /etc/rc.conf - regexp: ^strongswan_enable= - line: 'strongswan_enable="YES"' diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 003c4761..de3a9f1d 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,5 +1,11 @@ --- - block: + - name: Include WireGuard role + include_role: + name: wireguard + tags: wireguard + when: wireguard_enabled and ansible_distribution == 'Ubuntu' + - name: Ensure that the strongswan group exist group: name=strongswan state=present @@ -9,25 +15,25 @@ - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' - - include_tasks: freebsd.yml - when: ansible_distribution == 'FreeBSD' - - name: Install strongSwan package: name=strongswan state=present - - include_tasks: ipsec_configuration.yml - - include_tasks: openssl.yml + - import_tasks: ipsec_configuration.yml + - import_tasks: openssl.yml tags: update-users - - include_tasks: distribute_keys.yml - - include_tasks: client_configs.yml + - import_tasks: distribute_keys.yml + - import_tasks: client_configs.yml delegate_to: localhost become: no tags: update-users - - meta: flush_handlers - - name: strongSwan started - service: name=strongswan state=started + service: + name: strongswan + state: started + enabled: true + + - meta: flush_handlers rescue: - debug: var=fail_hint tags: always diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index af19ae2b..acd966c6 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -9,7 +9,7 @@ file: dest: configs/{{ IP_subject_alt_name }}/pki state: absent - when: easyrsa_reinit_existent|bool == True + when: keys_clean_all|bool == True - name: Ensure the pki directories exist file: @@ -49,7 +49,7 @@ -keyout private/cakey.pem -out cacert.pem -x509 -days 3650 -batch - -passout pass:"{{ easyrsa_CA_password }}" && + -passout pass:"{{ CA_password }}" && touch {{ IP_subject_alt_name }}_ca_generated args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" @@ -75,14 +75,14 @@ -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName }}")) -keyout private/{{ IP_subject_alt_name }}.key -out reqs/{{ IP_subject_alt_name }}.req -nodes - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" -batch && {{ 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 }}")) -days 3650 -batch - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ IP_subject_alt_name }}" && touch certs/{{ IP_subject_alt_name }}_crt_generated args: @@ -97,14 +97,14 @@ -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ item }}" -batch && {{ openssl_bin }} ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) -days 3650 -batch - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated args: @@ -121,7 +121,7 @@ -export -name {{ item }} -out private/{{ item }}.p12 - -passout pass:"{{ easyrsa_p12_export_password }}" + -passout pass:"{{ p12_export_password }}" args: chdir: "configs/{{ IP_subject_alt_name }}/pki/" executable: bash @@ -150,7 +150,7 @@ shell: > {{ openssl_bin }} ca -gencrl -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt register: gencrl @@ -165,7 +165,7 @@ shell: > {{ openssl_bin }} ca -gencrl -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ IP_subject_alt_name }}")) - -passin pass:"{{ easyrsa_CA_password }}" + -passin pass:"{{ CA_password }}" -out crl/algo.root.pem when: - gencrl is defined diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/vpn/templates/client_ipsec.conf.j2 index 7fde04ab..a45d8e3d 100644 --- a/roles/vpn/templates/client_ipsec.conf.j2 +++ b/roles/vpn/templates/client_ipsec.conf.j2 @@ -6,7 +6,7 @@ conn ikev2-{{ IP_subject_alt_name }} compress=no dpddelay=35s -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +{% if algo_windows %} ike={{ ciphers.compat.ike }} esp={{ ciphers.compat.esp }} {% else %} diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index e98bb3c1..086e18af 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -10,7 +10,7 @@ conn %default compress=yes dpddelay=35s -{% if Win10_Enabled is defined and Win10_Enabled == "Y" %} +{% if algo_windows %} ike={{ ciphers.compat.ike }} esp={{ ciphers.compat.esp }} {% else %} @@ -28,7 +28,7 @@ conn %default right=%any rightauth=pubkey rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} -{% if local_dns|d(false)|bool == true or dns_encryption|d(false)|bool == true %} +{% if algo_local_dns or dns_encryption %} rightdns={{ local_service_ip }} {% else %} rightdns={% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 9a342b4b..44fbcbda 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -7,13 +7,13 @@ IKEv2 -{% if (OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y') or (OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y') %} +{% if algo_ondemand_wifi or algo_ondemand_cellular %} OnDemandEnabled 1 OnDemandRules -{% if OnDemandEnabled_WIFI_EXCLUDE is defined and OnDemandEnabled_WIFI_EXCLUDE != '_null' %} -{% set WIFI_EXCLUDE_LIST = OnDemandEnabled_WIFI_EXCLUDE.split(',') %} +{% if algo_ondemand_wifi_exclude != '_null' %} +{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|string).split(',') %} Action Disconnect @@ -30,7 +30,7 @@ {% endif %} Action -{% if OnDemandEnabled_WIFI is defined and OnDemandEnabled_WIFI == 'Y' %} +{% if algo_ondemand_wifi %} Connect {% else %} Disconnect @@ -42,7 +42,7 @@ Action -{% if OnDemandEnabled_Cellular is defined and OnDemandEnabled_Cellular == 'Y' %} +{% if algo_ondemand_cellular %} Connect {% else %} Disconnect diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 820589f3..49c34e2f 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -70,7 +70,7 @@ COMMIT -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +{% if BetweenClients_DROP %} {% set BetweenClientsPolicy = "DROP" %} {% endif %} -A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index 4f00c309..a6d853f2 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -85,7 +85,7 @@ COMMIT -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP is defined and BetweenClients_DROP == "Y" %} +{% if BetweenClients_DROP %} {% set BetweenClientsPolicy = "DROP" %} {% endif %} -A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml deleted file mode 100644 index 0559c50b..00000000 --- a/roles/wireguard/defaults/main.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" -wireguard_interface: wg0 -wireguard_network_ipv4: - subnet: 10.19.49.0 - prefix: 24 - gateway: 10.19.49.1 - clients_range: 10.19.49 - clients_start: 100 -wireguard_network_ipv6: - subnet: 'fd9d:bc11:4021::' - prefix: 48 - gateway: 'fd9d:bc11:4021::1' - clients_range: 'fd9d:bc11:4021::' - clients_start: 100 -wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" -wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" -easyrsa_reinit_existent: false -wireguard_dns_servers: >- - {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} - {{ local_service_ip }} - {% else %} - {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} - {% endif %} diff --git a/roles/wireguard/meta/main.yml b/roles/wireguard/meta/main.yml deleted file mode 100644 index a766ccc1..00000000 --- a/roles/wireguard/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - { role: common, tags: common } diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml index 322f974f..b38ab1fb 100644 --- a/roles/wireguard/tasks/keys.yml +++ b/roles/wireguard/tasks/keys.yml @@ -3,7 +3,7 @@ file: dest: "/etc/wireguard/private_{{ item }}.lock" state: absent - when: easyrsa_reinit_existent|bool == True + when: keys_clean_all|bool == True with_items: - "{{ users }}" - "{{ IP_subject_alt_name }}" @@ -13,7 +13,6 @@ register: wg_genkey args: creates: "/etc/wireguard/private_{{ item }}.lock" - executable: bash with_items: - "{{ users }}" - "{{ IP_subject_alt_name }}" diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index df5b832e..232d080c 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -4,7 +4,7 @@ repo: ppa:wireguard/wireguard state: present register: result - until: result|succeeded + until: result is succeeded retries: 10 delay: 3 diff --git a/server.yml b/server.yml new file mode 100644 index 00000000..c71b5be1 --- /dev/null +++ b/server.yml @@ -0,0 +1,65 @@ +--- +- name: Configure the server and install required software + hosts: vpn-host + gather_facts: false + tags: algo + become: true + vars_files: + - config.cfg + + roles: + - role: common + - role: dns_adblocking + when: algo_local_dns + tags: dns_adblocking + - role: ssh_tunneling + when: algo_ssh_tunneling + tags: ssh_tunneling + - role: vpn + tags: vpn + + post_tasks: + - block: + - name: Delete the CA key + local_action: + module: file + path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + state: absent + become: false + when: not algo_store_cakey + + - name: Dump the configuration + local_action: + module: copy + dest: "configs/{{ IP_subject_alt_name }}/config.yml" + content: | + server: {{ 'localhost' if inventory_hostname == 'localhost' else inventory_hostname }} + server_user: {{ ansible_ssh_user }} + {% if algo_provider != "local" %} + ansible_ssh_private_key_file: {{ ansible_ssh_private_key_file|default(SSH_keys.private) }} + {% endif %} + algo_provider: {{ algo_provider }} + algo_server_name: {{ algo_server_name }} + algo_ondemand_cellular: {{ algo_ondemand_cellular }} + algo_ondemand_wifi: {{ algo_ondemand_wifi }} + algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude }} + algo_local_dns: {{ algo_local_dns }} + algo_ssh_tunneling: {{ algo_ssh_tunneling }} + algo_windows: {{ algo_windows }} + algo_store_cakey: {{ algo_store_cakey }} + IP_subject_alt_name: {{ IP_subject_alt_name }} + {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} + become: false + + - debug: + msg: + - "{{ congrats.common.split('\n') }}" + - " {{ congrats.p12_pass }}" + - " {% if algo_store_cakey %}{{ congrats.ca_key_pass }}{% endif %}" + - " {% if algo_provider != 'local' %}{{ congrats.ssh_access }}{% endif %}" + tags: always + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always diff --git a/tests/local-deploy.sh b/tests/local-deploy.sh index b586aaac..fc7d038e 100755 --- a/tests/local-deploy.sh +++ b/tests/local-deploy.sh @@ -2,12 +2,11 @@ set -ex -DEPLOY_ARGS="server_ip=$LXC_IP server_user=ubuntu IP_subject_alt_name=$LXC_IP local_dns=true dns_over_https=true apparmor_enabled=false install_headers=false" -touch /tmp/ca_password +DEPLOY_ARGS="provider=local server=$LXC_IP ssh_user=ubuntu endpoint=$LXC_IP apparmor_enabled=false ondemand_cellular=true ondemand_wifi=true ondemand_wifi_exclude=test local_dns=true ssh_tunneling=true windows=true store_cakey=true install_headers=false tests=true" if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v /tmp/ca_password:/tmp/ca_password -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook deploy.yml -t cloud,local,vpn,dns,ssh_tunneling,security,tests,dns_over_https -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "DEPLOY_ARGS=${DEPLOY_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook main.yml -e \"${DEPLOY_ARGS}\" --skip-tags apparmor" else - ansible-playbook deploy.yml -t cloud,local,vpn,dns,dns_over_https,ssh_tunneling,tests -e "${DEPLOY_ARGS}" --skip-tags apparmor + ansible-playbook main.yml -e "${DEPLOY_ARGS}" --skip-tags apparmor fi diff --git a/tests/update-users.sh b/tests/update-users.sh index bea5a8cb..ba40bb33 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -2,16 +2,13 @@ set -ex -CAPW=`cat /tmp/ca_password` -USER_ARGS="server_ip=$LXC_IP server_user=ubuntu ssh_tunneling_enabled=y IP_subject=$LXC_IP easyrsa_CA_password=$CAPW apparmor_enabled=false install_headers=false" - -sed -i 's/- jack$/- jack_test/' config.cfg +USER_ARGS="{ 'server': '$LXC_IP', 'users': ['user1', 'user2'] }" if [ "${LXC_NAME}" == "docker" ] then - docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\" -t update-users --skip-tags common" + docker run -it -v $(pwd)/config.cfg:/algo/config.cfg -v ~/.ssh:/root/.ssh -v $(pwd)/configs:/algo/configs -e "USER_ARGS=${USER_ARGS}" travis/algo /bin/sh -c "chown -R 0:0 /root/.ssh && source env/bin/activate && ansible-playbook users.yml -e \"${USER_ARGS}\" -t update-users" else - ansible-playbook users.yml -e "${USER_ARGS}" -t update-users --skip-tags common + ansible-playbook users.yml -e "${USER_ARGS}" -t update-users fi if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.crt | grep CRL @@ -22,7 +19,7 @@ if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.cr exit 1 fi -if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/jack_test.crt | grep CN=jack_test +if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/user1.crt | grep CN=user1 then echo "The new user exists" else diff --git a/users.yml b/users.yml index f60cbb3b..36f162f5 100644 --- a/users.yml +++ b/users.yml @@ -1,5 +1,4 @@ --- - - hosts: localhost gather_facts: False tags: always @@ -8,27 +7,43 @@ tasks: - block: + - pause: + prompt: "Enter the IP address of your server: (or use localhost for local installation)" + register: _server + when: server is undefined + + - name: Set facts based on the input + set_fact: + algo_server: >- + {% if server is defined %}{{ server }} + {%- elif _server.user_input is defined and _server.user_input != "" %}{{ _server.user_input }} + {%- else %}omit{% endif %} + + - name: Import host specific variables + include_vars: + file: "configs/{{ algo_server }}/config.yml" + + - pause: + prompt: Enter the password for the private CA key + echo: false + register: _ca_password + when: ca_password is undefined + + - name: Set facts based on the input + set_fact: + CA_password: >- + {% if ca_password is defined %}{{ ca_password }} + {%- elif _ca_password.user_input is defined and _ca_password.user_input != "" %}{{ _ca_password.user_input }} + {%- else %}omit{% endif %} + - name: Add the server to the vpn-host group add_host: - hostname: "{{ server_ip }}" - groupname: vpn-host - ansible_ssh_user: "{{ server_user }}" + name: "{{ algo_server }}" + groups: vpn-host + ansible_ssh_user: "{{ server_user|default('root') }}" + ansible_connection: "{% if algo_server == 'localhost' %}local{% else %}ssh{% endif %}" ansible_python_interpreter: "/usr/bin/python2.7" - ssh_tunneling_enabled: "{{ ssh_tunneling_enabled }}" - easyrsa_CA_password: "{{ easyrsa_CA_password }}" - IP_subject: "{{ IP_subject_alt_name }}" - ansible_ssh_private_key_file: "{{ SSH_keys.private }}" - - - name: Wait until SSH becomes ready... - local_action: - module: wait_for - port: 22 - host: "{{ server_ip }}" - search_regex: "OpenSSH" - delay: 10 - timeout: 320 - state: present - become: false + CA_password: "{{ CA_password }}" rescue: - debug: var=fail_hint tags: always @@ -41,22 +56,17 @@ become: true vars_files: - config.cfg - - pre_tasks: - - block: - - name: Common pre-tasks - include_tasks: playbooks/common.yml - tags: always - rescue: - - debug: var=fail_hint - tags: always - - fail: - tags: always + - "configs/{{ inventory_hostname }}/config.yml" roles: - - { role: ssh_tunneling, tags: always, when: ssh_tunneling_enabled is defined and ssh_tunneling_enabled == "y" } - - { role: wireguard, tags: [ 'vpn', 'wireguard' ], when: wireguard_enabled } - - { role: vpn } + - role: common + - role: ssh_tunneling + when: algo_ssh_tunneling + - role: wireguard + tags: [ 'vpn', 'wireguard' ] + when: wireguard_enabled + - role: vpn + tags: vpn post_tasks: - block: From 36c871c4f1ceb83ab41a9158b1977b1964484e7b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 17:28:02 +0300 Subject: [PATCH 657/769] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 897352b7..8b6969fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 27 Aug 2018 +### Changed +- Large refactor to support Ansible 2.5. [Details](https://github.com/trailofbits/algo/pull/976) + +### How to upgrade +- Follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch + ## 04 Jun 2018 ### Changed - Switched to [new cipher suite](https://github.com/trailofbits/algo/issues/981) From 701995ebb72d321f0557f180da650648e4f8d310 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 17:29:16 +0300 Subject: [PATCH 658/769] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b6969fb..63a4a450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 27 Aug 2018 ### Changed - Large refactor to support Ansible 2.5. [Details](https://github.com/trailofbits/algo/pull/976) +- Add a new cloud provider - Vultr ### How to upgrade - Follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch From 511086db8ef4b82c1887a28dcde718f41f17d715 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 19:00:32 +0300 Subject: [PATCH 659/769] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a4a450..8fb954fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ - Large refactor to support Ansible 2.5. [Details](https://github.com/trailofbits/algo/pull/976) - Add a new cloud provider - Vultr -### How to upgrade +### Upgrade notes - Follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch +- You can't update users on your old servers with the new code. Use the old code before this release or rebuild the server from scratch ## 04 Jun 2018 ### Changed From 5f9a3d5eb5d1cc133c7b7d08ebd43c970efc9677 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 19:01:59 +0300 Subject: [PATCH 660/769] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb954fd..b0b7c7c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Add a new cloud provider - Vultr ### Upgrade notes -- Follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch +- If any problems encountered follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch - You can't update users on your old servers with the new code. Use the old code before this release or rebuild the server from scratch ## 04 Jun 2018 From 635e7ff1af271ef91749a96e05b8f8a258c57106 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 27 Aug 2018 20:23:51 +0300 Subject: [PATCH 661/769] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f5ca043..176f9acc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server +* Installs to DigitalOcean, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server ## Anti-features @@ -29,7 +29,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon Lightsail](https://aws.amazon.com/lightsail/), [Amazon EC2](https://aws.amazon.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [OpenStack](https://www.openstack.org/). +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or an OpenStack based cloud hosting. 2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. From 6d3bb1cf2baadb4b552beaa9e726216d7ea56d54 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 28 Aug 2018 10:03:43 -0400 Subject: [PATCH 662/769] Update minimum required IAM changes for deployment (#1080) Ansible2.5 allows Algo to directly ask AWS for the region list, rather than have it hardcoded and updated manually. Updated the documented minimum required permissions to include "DescribeRegions". --- docs/deploy-from-ansible.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index f3566c7f..946c045b 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -113,6 +113,7 @@ Additional variables: "Action": [ "ec2:DescribeImages", "ec2:DescribeKeyPairs", + "ec2:DescribeRegions", "ec2:ImportKeyPair" ], "Resource": [ From 3144458ac7b99372a301f2a178010c2fcdf37396 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 28 Aug 2018 10:05:01 -0400 Subject: [PATCH 663/769] Update cloud-amazon-ec2.md (#1081) --- docs/cloud-amazon-ec2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/cloud-amazon-ec2.md b/docs/cloud-amazon-ec2.md index 63831d55..36c51359 100644 --- a/docs/cloud-amazon-ec2.md +++ b/docs/cloud-amazon-ec2.md @@ -112,3 +112,6 @@ Enter the number of your desired region: ``` You will then be asked the remainder of the standard Algo setup questions. + +## Cleanup +If you've installed Algo onto EC2 multiple times, your AWS account may become cluttered with unused or deleted resources e.g. instances, VPCs, subnets, etc. This may cause future installs to fail. The easiest way to clean up after you're done with a server is to go to "CloudFormation" from the console and delete the CloudFormation stack associated with that server. Please note that unless you've enabled termination protection on your instance, deleting the stack this way will delete your instance without warning, so be sure you are deleting the correct stack. From f63bc1ef970266ec57b1d5a0806ca1dba2c175d4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 28 Aug 2018 17:12:20 +0300 Subject: [PATCH 664/769] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b7c7c9..e7f566a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Upgrade notes - If any problems encountered follow the [instructions](https://github.com/trailofbits/algo#deploy-the-algo-server) from scratch - You can't update users on your old servers with the new code. Use the old code before this release or rebuild the server from scratch +- Update AWS IAM permissions for your user as per [issue](https://github.com/trailofbits/algo/issues/1079#issuecomment-416577599) ## 04 Jun 2018 ### Changed From ee3cb979f770e92899627b70c013a6d8a90a33a8 Mon Sep 17 00:00:00 2001 From: David Myers Date: Tue, 28 Aug 2018 10:25:40 -0400 Subject: [PATCH 665/769] Document how to use WireGuard on Ubuntu clients (#1071) --- README.md | 1 + docs/client-linux-wireguard.md | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 docs/client-linux-wireguard.md diff --git a/README.md b/README.md index 176f9acc..26440fd3 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ After this process completes, the Algo VPN server will contains only the users l * Client setup - Setup [Android](docs/client-android.md) clients - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible + - Setup Ubuntu clients to use [WireGuard](docs/client-linux-wireguard.md) * Cloud setup - Configure [Amazon EC2](docs/cloud-amazon-ec2.md) - Configure [Azure](docs/cloud-azure.md) diff --git a/docs/client-linux-wireguard.md b/docs/client-linux-wireguard.md new file mode 100644 index 00000000..123ab76e --- /dev/null +++ b/docs/client-linux-wireguard.md @@ -0,0 +1,48 @@ +# Using Ubuntu Server as a Client with WireGuard + +## Install WireGuard + +To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from an Ubuntu Server 16.04 (Xenial) or 18.04 (Bionic) client, first install WireGuard on the client: + +``` +# Add the WireGuard repository: +sudo add-apt-repository ppa:wireguard/wireguard +# Update the list of available packages (not necessary on Bionic): +sudo apt update +# Install the tools and kernel module: +sudo apt install wireguard +``` + +(For installation on other Linux distributions, see the [Installation](https://www.wireguard.com/install/) page on the WireGuard site.) + +## Locate the Config File + +The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your Linux client. + +If your client is running Bionic (or another Linux that uses `systemd-resolved` for DNS) you should first edit the config file. Comment out the line that begins with `DNS =` and replace it with: +``` +PostUp = systemd-resolve -i %i --set-dns=172.16.0.1 --set-domain=~. +``` +Use the IP address shown on the `DNS =` line (for most, this will be `172.16.0.1`). If the `DNS =` line contains multiple IP addresses, use multiple `--set-dns=` options. + +## Configure WireGuard + +Finally, install the config file on your client as `/etc/wireguard/wg0.conf` and start WireGuard: + +``` +# Install the config file to the WireGuard configuration directory on your +# Bionic or Xenial client: +sudo install -o root -g root -m 600 .conf /etc/wireguard/wg0.conf +# Start the WireGuard VPN: +sudo systemctl start wg-quick@wg0 +# Check that it started properly: +sudo systemctl status wg-quick@wg0 +# Verify the connection to the Algo VPN: +sudo wg +# See that your client is using the IP address of your Algo VPN: +curl ipv4.icanhazip.com +# Optionally configure the connection to come up at boot time: +sudo systemctl enable wg-quick@wg0 +``` + +(If your Linux distribution does not use `systemd`, you can bring up WireGuard with `sudo wg-quick up wg0`). \ No newline at end of file From e860b78d804fc77b4c87359de17f0df7555219f3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 29 Aug 2018 16:05:07 +0300 Subject: [PATCH 666/769] Scaleway authentication fix (#1088) --- roles/cloud-scaleway/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index 9242fb3a..ecf52e95 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -54,7 +54,7 @@ method: GET headers: Content-Type: 'application/json' - X-Auth-Token: "{{ scaleway_auth_token }}" + X-Auth-Token: "{{ algo_scaleway_token }}" status_code: 200 register: scaleway_images with_sequence: start=1 end={{ ((scaleway_pages.x_total_count|int / 100)| round )|int }} From fb1c0f6a5e692fb2dde905d1ea3c8b44dc10874e Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 30 Aug 2018 15:36:35 +0300 Subject: [PATCH 667/769] Create a symlink if deploying to localhost (#1078) --- server.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server.yml b/server.yml index c71b5be1..459dd63d 100644 --- a/server.yml +++ b/server.yml @@ -51,6 +51,14 @@ {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} become: false + - name: Create a symlink if deploying to localhost + file: + src: "{{ IP_subject_alt_name }}" + dest: configs/localhost + state: link + force: true + when: inventory_hostname == 'localhost' + - debug: msg: - "{{ congrats.common.split('\n') }}" From 687bab9e5478fd0c0aa7dddb488e3c7a10ef3350 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 30 Aug 2018 16:25:59 +0300 Subject: [PATCH 668/769] Update troubleshooting.md Fixes #744 --- docs/troubleshooting.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 632696d9..e6717b7a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -8,8 +8,9 @@ * [Error: "ansible-playbook: command not found"](#error-ansible-playbook-command-not-found) * [Bad owner or permissions on .ssh](#bad-owner-or-permissions-on-ssh) * [The region you want is not available](#the-region-you-want-is-not-available) - * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) - * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) + * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) + * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) + * [DigitalOcean: error tagging resource 'xxxxxxxx': param is missing or the value is empty: resources](#digitalocean-error-tagging-resource) * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -163,6 +164,26 @@ Algo builds a [Cloudformation](https://aws.amazon.com/cloudformation/) template In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must either [delete the VPCs from previous deployments](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/working-with-vpcs.html#VPC_Deleting), or [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. +### DigitalOcean: error tagging resource + +You tried to deploy to Algo to DigitalOcean and you received an error like this one: + +``` +TASK [cloud-digitalocean : Tag the droplet] ************************************ +failed: [localhost] (item=staging) => {"failed": true, "item": "staging", "msg": "error tagging resource '73204383': param is missing or the value is empty: resources"} +failed: [localhost] (item=dbserver) => {"failed": true, "item": "dbserver", "msg": "error tagging resource '73204383': param is missing or the value is empty: resources"} +``` + +The error is caused because Digital Ocean changed its API to treat the tag argument as a string instead of a number. + +1. Download [doctl](https://github.com/digitalocean/doctl) +2. Run `doctl auth init`; it will ask you for your token which you can get (or generate) on the API tab at DigitalOcean +3. Once you are authorized on DO, you can run `doctl compute tag list` to see the list of tags +4. Run `doctl compute tag delete enivronment:algo --force` to delete the environment:algo tag +5. Finally run `doctl compute tag list` to make sure that the tag has been deleted +6. Run algo as directed + + ## Connection Problems Look here if you deployed an Algo server but now have a problem connecting to it with a client. From 0188b2ff64544458585f2871fe96f3bc87dd1618 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 30 Aug 2018 16:40:01 +0300 Subject: [PATCH 669/769] Update deploy-to-ubuntu.md --- docs/deploy-to-ubuntu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 36956a30..6d6db62b 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -8,7 +8,7 @@ tl;dr: ```shell sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible python-pip build-essential python-dev +sudo apt-get update && sudo apt-get install ansible python-pip build-essential python-dev libssl-dev libffi-dev pip install virtualenv pip install --upgrade pip git clone https://github.com/trailofbits/algo From 002c4ef198aa0fea78ba8355dceec5a7e70411ba Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 31 Aug 2018 08:40:22 +0300 Subject: [PATCH 670/769] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e94593d3..7a8982df 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ ### OS / Environment (where do you run Algo on) ``` @@ -9,7 +9,8 @@ PUT THE OUTPUT HERE ### Cloud Provider (where do you deploy Algo to) ``` @@ -29,7 +30,7 @@ PUT THE OUTPUT HERE 3. ### Full log - + ``` PUT THE OUTPUT HERE From d9634eca8a3c9454c7c6e044b788f988054b16db Mon Sep 17 00:00:00 2001 From: Mike Myers Date: Sun, 2 Sep 2018 03:32:51 -0700 Subject: [PATCH 671/769] Update screenshot of AWS EC2 minimum permissions with ec2:DescribeRegions (#1095) --- docs/images/aws-ec2-new-policy.png | Bin 82947 -> 87193 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/aws-ec2-new-policy.png b/docs/images/aws-ec2-new-policy.png index 47d7f283a681a3cf4b6fe716852f21764ea16843..691512e7f96eccaef4b4955a69aad174fa64efea 100644 GIT binary patch delta 81219 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�kLZS3o!<|cb$~uDnP!d=z;L5&=mN=_{wU0 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

Ch+R0DT~@A(zDT81Y4& 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 LsX1@Oib?XK~ delta 76939 zcmc$`WpG|evMp*cTFlH07Bg5Zi3&WpJ3 zzBqC34@D^UuG*QEm6^3FSJjtN4B;OFK^!VCEB+A{2lm5<4<98ZL=-=K0KNbHM+*)4 z`-!E0!+ABa0X7I%oIf@f2wPk`wlwgAEPwCA2f+`LBA=CAK~BtcJ?KHW!3KmPyPU)>Vr!A zzyUC&v8M(wCBXH{Ia9 zYXoqf9&^g-;P(}OReQ}>hJ>y8r9MJ%zkBxlU7;>;0t!$SniYZjQ6xIgdznx*b8~8& zmC<0iSHRtDx{27`={#cK`KB+^boBG(>9Q+L8DYTpOvfj>xwnhBsLK#yIm6MS>}jW; z=P3_c$F@%Q?n(m&M}78G)YRQM`FcISdhw4tp5V(h*@MM$W469II0zL+o#yGKFV$Bc z)G|7~JqG;K#Q(GsuQYgxGW~u}KOpV-wm$2+*DsJObN|Va@DnIsuhS@-(`8XZDjPqasf!0C{7q!UIHU7_{viEPVv z1L#8*EKQLc z7zOG27z7@h^Fy@Lxu0geMRr+PgtGm~WWADjnq21xOjA=wn8U1A${sWtfu5Hdk4#6j z5`mtOC?VB5l|$`-Z_JMoH*s_ih<|sY52&sZFv^F6?QAAOh`Ly6YHEiM$G{sW^lW;I zuG3_TahB0MNmj9=R{%G(I7(D*Lm|`>c>VV?7#C~J`tRHzt=_AQi|PCxy4+ECPiSv1 z)7P;-qKvwMzj@woH|!!6%=#jJ2GI%G!GAEt2F@puPf;ZL4Gj%r>pmPHs?t!xW<2}} zM~X4^lTb=z0jg%pC*6I2N1dr$q?TN2@TKfcJw?GVQTuhCpO& z^vDZj|Ea-BkiF3#Q5wG}x4W`ktzL~e8qQWCx3-pJdEWjJP`^ifAMBIti+)_7$!Z&9 zOiLQcF5M(Ry`lccyJ!qw8uYV3oH3WuH!u&yS^ybNyHa~B=74{#73$>lbR=d?w1g|n z?)~=1`BeEg@u9KJTuQQfJo~Bifalp`?*Mga+EK=8kNn(+xNr2B!s2oweb<{<98cKA zUy+|&Z9~>?hLDq)zcEZRYR@zSXtHguU`Q)<#PW{V*oW8>BOYiDV{V>C(5`HPX03uv zEdbi~Q}Z{@5P}#6rpX@5%n~ISd(GdPyJEp8r`R!qKLKNz zq%e0vg>Pdn7+Z>}4SkbCL&$obS0GRKr^bDb^BG_d&&QQt7(t0&!u#tC?aOim0P_7G zMwGmVH>Y2Xy*$W?s%P`n`)I{N{FWPI0I@4^0Twjhyshi*w)xAgos_U&!Oshi8fDMV z>fp03?8BzxDOjVK4QNo2R_CYvv{Gtc9R|1sH>ss@?l4f!tuGK=%-$vpJ$ArdQ%CY! zurvx?w#%_cQmB_U{da4T=6+%ZMXDRzQF}g)YdzeRars!ebV7&RX%&f$Cvzi@1LLxa zb@_E~hdAZUhX0&D6{)OY$Fo#+b2^wAl1D?pU9<{w-mvm{sh+Q&I`8AjE{miNWuW6_ ze`2y+;9Xq9C|d4a7;?*f$J(uA)j1^c+hRPcHW51EH?dK)GzsoV7k=#su5*U?w3~w^ z7(?5a;B{W#+f3<6xVxKI;6uM_BTVm>N?gJS4Hq{}#OVkTt&WN$E23!@j>llB{HW1H zH?}uE)YnI#wBcp6PkXFv$zV1S0?UYGAzY+|!z@}ukFJ-q(jsw3oQlh)fbveT+7h7B~1!FYTL(xWQDi?7?y);}M$|_EUw%Z2x^#Al=2}+&kd}1PrH^(Yp04y2Bodj)bh1~K-a^{j z)9i!T=nvR9*CTk@bpArAR;*-&4y~Np9n?S9pQb|ai2O4TtU4?OIhpr#3H@tHPTmcA51&b@mi8+9?8$LWO(hS^_e^GeElX4K*B16ZTJ>?t?B zJiCEI-h&L^87@#RUo6+1<^B8)m*6b1+InBB&by5b&Q?TP%z>uY>JvG_-z8ox6iD-d z5Ga3wa-X5^eXO#Z< zF>~}4t9mKrYRyZ4y2AmcQ#X%|q|;LV zZdqvN^p$h%fFjQwk@YGc%)VOJd+QBOo!5)V4`)898FO9mDYRYvV(&}3h1|hNEz?(5 zxd&S9%6!8CHL1(Z26*a;?KO=s>eY7p+Xrsm`#X~%2D=$6&qwBRk0Gfpy#XL13ee<8 zin5|es62DM48@@0f5d;9;0$SeV*~SVsq@h;>mTH8CzN`xR#vM(-~AjWe_sff0kl7mfJ>K)ASY9Fi*c_#zDl z!bW2Sc0LRkOR9)V>VL6-tT9)x0VA}{Gk!rZK9;fv-n)A_y(mOc_WCP$C!$<6FCnv! zn+>j=>0XSQ$IlG-o8wC&^jnBdD%UrdX3*G#Q zLx;(}qxImQ-KH786SJsbJQGh2_(<+qE+ zTk3}q$SLgV)xI~h;%|6Q(g9~HvF+pCue|nUqHq(t;xlX60`8A-Gc*xTpm*unUqyFa z>JZ=VyRR=$b+2l^$HK9ip9NWTX@kcdkhT$5WSP}gRmtEUVVs?XJUx5W|R< z3$;=8H}+yAWLo#1(@N8ui)7GiAn6xLa{N7!Ait34$%pR*5|4_=J(h(#To?j}^BzLS z;3=YTBX{-6yCc+gu+=WfFW)_HGSE^M4(SY!=%AgX+5ZIZcDB&U#hS9eaiOc#29@*+ zFoLVz3>O5U=w8~lV^X7sDtwA}^(#BC^#fDy_hL3pSto!4cZ>lE;0s4K^r0taHKgBl9gqsup+yHACx15ezY5(Z#Jm zKdG%UOk?~pS#=%eisMYNJUW&WpXVuBRx-3iCD;HTWEFZ~{6lAhcCr zXMM$F%)~a(`}T*P1o$%FSt7jwIMO83;sWK)pdNjuKsxW^5ne`P+_g+LINO(Lb;^ZI zbsj02`3}O<1fc*AkAUDORc$Ea`KL|@9BT_7xPjO@Kd%q`dhdK&4etRrBT4p;SLoXO zjqvF=9Xl*4+;bi87$#%k1K0N?>p>IZy1crV)*>%I%`=I+0DaD&9QqajNq0on#(w?w*8bKI475mD8C_!*IRBnt|BV#V1cd+#%C{ zwRbt#mk!wf;5ZG-5jeD5O_qxYDr^>W`>^@YtfJFWDfAix5LyNTL_3So_K%C?@};M{ z3V_4Zry_E1t#t5Pr8l%_LfsdlAKv|gl5(Wo_m*u>qIhFyO`(HU92t@1#8m9;s#i*~ zoH@^kYek`8A0MWpA-n5;g2w7^k4DR5?~fp&P9dI2%M-G<+p|Pv`MG& zw-6F`JQ0!s_o)TwZz>PFae-9uhrxm}+==r0^jC7q61p;zdKoq`q_M`{-m?jF@VHb-6iI-NkXBoMP-{sC*=MY6v}Hdl=(%YR_zO z>{mLroU`GXejC#fwTf;%y>vP)tQS7#t@ye{-v>}G+<#&;7jT+K)n*su_4Ds1hoaiR z$cC|J=XPi2Wqb7v+(}eue{=7-;l!{U(`GZW+2GZdi8m+4-@4laL&R?2Jz=9Ojr;vpoXKyT-@G@lKwbE zg3D3r%YzNAeARWw?#2lY8=0^XfMh!gkDjv;b}&IGj`+?UpCr%{$REunu9}4ijKvNW zpaqiON>&F(q{B5U4NnGgqi9k(R^87qdY{kb$Gin~r#x+8_pw_@wB|K+H#P)&7Ak*C zXt*{$pM$>DsuRW4lt2H~RlUWF6+kb0S_7A5X7=oYpS8-&`Vlx1fonRR?F~bH`V8VG z_8BJY!=jqs#@QYM6p!)JCc$1Ru)qy#hq2Yy-G|OmLSDY7xrj znpO$A}2#wyBKJ2ZRw*$S!~-Y)HoiuN$MZ%~9s_y7`3`qb9q{CnsGe_Hee z{}bHG_JpNM$V|wyvaC1|U6s9%;{K6b^pO*|z#_r$46jqEoziVbUG>w7A>u&r__kxm z7f%gsy^z3-1uZ!%S(S9Pu|pQ=9FBaxSMM!)qTx2xjC8^FBMv<=(`0bnj_j3E=hFLp*$6mz*Gd>6^~0a z_{ujb+Eo$yXSkG(y6*goYwA5LrCl|TJMLLul4io*UN<{Gl}lL!x_v71SJda2|5~n^ zAFc}bb!>Xbbu|3u6zs_0Dj1iEz0a=Xvt$A%cTbe#DY=r}`Sq&Jg~Vzj%+HU?y%#lQu=WPyONDfg{%7pY71QkR}u|< zf%Vb4x+YwHYd7UTk;DWvbn!_>s>{>#83Ec6W$>xp=1)GvwSo_nj^OTW(?m&-Q{%dn ztG0>+=h8gJ{9;QON6a6+xeFs?m(a97Iv7O{I(2RUkN4tTfdO3^aciGn0u0d-V}FF# zDFqLH!DtYF59%#Jb&Z%|Y-?!`yIJqC*Vv8tu%sfId~8Ebe%KVR~I*Y)%9&po(F+Y zfF9RBCO-$F_v6lCR1d1w#cn^H=ix4#?a?=QZ&OBRMA@JMdmeXo8q#OhSy8H1DmE?NWX{GV5SEcKJU_%KxZW#@Oamu`WToP#V+Wg8tOz(M82`h1I+^Jig1>Krlyp zMs~gy#EaTZP@+Y)G~Y9}dVO&eh9z;#y6RQ$*N&5NdCYV@Tb@U&1NRNGGYqh%=0XE8 zpM_$L3lvvDXrNCIiymLxe&(Q@46!cAQ{9;_z zo=`jm#B}rcmKbG|k~E_-*Hc?gNj-*gX?*1`GmDf2y+1nqtcRYSDA$!p-Im?4%=WUf z)HP6a{S1w_gdLI2>^r_mc$GCpRq9sBMR&(zzKg@nS8kCXMUcTkA~*q))e~Q#5s>hg zh0+#o_$h41RcmhL31;i`MXc;TUG-B>55V_3t;Br2>A!+-wO+~#W}Zl+{v*}Cx>-XHQ*cDxOq5@?MC)wMPzUo7IIA38jMME?aEK(q8C349O}E zJaQ`gBLcO(^Pzj503WZjalc#B4|~|L7QWy!{G%9~Z^#T}_}vYCLacZ9!L=Vm3dr73 zn17kMq%o~-9%}~7*v@UM$LT-JT?eEWd`LoxmZSk+D<=@P&OSEMx6$JXA04HQm088$ zQ<(3S5GWP7!R!=iOTi&4Nq%{{y+Q^0@CKa-*!Pj07>!a(cgBdZlb zmd>CCHc+yf*3|gDiSY4F&4|qws&Z+miLJjW206OHD?=D=>5}O-6^rbVi-a*e zz%nTj+H4icBA>x^(kVRXX(m8E^*?A9D6%U&f}vY zzCoALiifNH-2<`qQB*zt@G35H*Ry<#9Ea`T2WD1lkz2*Q_OkC-c$ARsQ`yMnDp{sy z&(<(Srh^OG1cVN)_Fu)e1~MByt7yM`-;b{y_(xH^sq;7A(S)gyU0%)98Vorx#iItT zpF%gjB_X2UPO@iE( z#J>x*J++h3NN}02Rz3f~YHYDIrAQ^Y7&BfCW-VD5X9JHSYoT0sppaWFOz-eD$M@^T znT?z%Z(TZxwYi0k*wtn){8k|_UFms>Xt{ImMd1zr*_m=L{^TF^mmZOu=uz6Ml_z18Fu!)+gH z<@KhD*ESeJ>X~^p#ll-y_t7TIy_L%}^g{j!?KJJ~C0ZhAX_{DNZq^!^ltfivO$opFl_c7Yy&G%{8Pn_2tDian*A3 zooru@@@-U)T`2)>m)t3LQ7>`DP2?k<$Gi?w{6pLXM`buK!@oxShoaIO$Q1JQ0ng&e z0;cJIC*uGgm=zY#Ikm~vS~E_f{}lY&T0cI(-K)qX_nQ8jr2T_o3jO{39)?x?pNz$y zivIs;5B%>6@X_(~Y->hrQFUs=}JKlq5~RC#D2~CTM2LLSeePJrftH@YLCCBXYdr z^Ou5ve{oQXu{$b>pKdAV&aJPkWuMKL;Bf{|3Y)dNgBRStd)t7@fdtT~C}aK#$SMgR zV{Bisgh0O3#x1J~UqRat>2e`mRmiB2^|h7*ej{k^=T_X7qPzXZ3hl1^)cm4+5MCUp zuKClclsf8mH{{Tv{XAv|>O0ZnDnV;E^le2&lGyM*W1OSsK3Sd3>hd8 z9Ti|&tbC<((R(Db3+@4hiTJ>J9f~?3mNK<6Xgo)nWKoa9NP}ifiRi5A_r{wZ>S=JLuB47zkEI zUD(gV1yE574u^s*05_mtfI6FrpEiQIkf=yI<{Qm8nR7Lm!y+~EwzhJ;_f}+};}tj1 zNEoe(YMe|M73lxUGs%J4t%q0{E-%}&^lh< zd{jkL4c0qYpmC-P+MX?Ja(!)@T7DGPmBj4_o#j$wd#H#5y_+omj{6OMWLL*Pk6F>a z{G(Z=qk3FJ-6{?$XphRfgY_eD=QP^yRWO0@`W7+j{!6;OMbxAu*QnGMQ+0%OPXF{yfjyfDFWxGTr5JtM!`@mgzYn9W#? z``U8ErA!pVF+MAw%zc@D!1!Cbg@;xq^Tp!yYLcx?<44dCp-h11A-p*~wN-*lb%OET zZU)o5#j7Vf@ils+9bo^u9W#O!1QpcwLej=a*uR$!Ge{rf#D}NJ3xrAz3(4L(;e|$a zSHlYj5|O)O)}65+y)JVo)4@ks7*P-iTCHfDp7?gtpB9J9S@PaG0uL};DcqkFN*fQa ziktXrE%C?A7&Q@bg5Sn&!u>1r@jstrY_~YldiN>7dLRW_Pm41-D!sj6M7>l&4&;^1 ziB5o&UwEt#YbRn<130bD7dPUWRiFG5IIdScoJiaHVm^uh|+*mE(yb3+{tgosP zccO`Nct*M|Nr=z*ZUQ(E0=XSAPHWClPE*x#RPOb$rXE+n?hIEM=rxk&Tn&T(_30(< zS|_$UlVP|fKEL3Ne%*#yfm&=+slSDv84Pa@(DO>4myY4rWVNCd*d>Q;rA9}OZ`nA| zM}0~xW_C+@({-xlpw>M;j?OWSV7Px4$NlPw9PjV{duvC-UKZJ}5=jnWi#|@U5JB54 zXXv;t=&xK9gHnsp`q2az-sVVFMQy_HmnNH-9IUWTdigGD@2l@Lf>js8X=2Qtr-VTz z(Ck-bpF1VAPWWkmX0SqCvxq2fPaAK^Yw#drGmu*V)q~yJzf_Jty2H2rqir_pIebpX zx>iN5;Q`hAm4?;p9cW7=Ryc!qgO!I;gw~R5keE_H4|DJ27ki6iO}xRGi8HJ<^!*>P z+GKA}qzt-*ep6&=9#0QFfh{tzh_f1bfc9}X!|j+O>lbGGCq$-wq6oN_NsP2V;nknv zZpR4Co72ZV=8MXDIQl3{LD|$ufH90VW{<&OhE3q9FmN7%vq(e2{EZRyebg;W};2lMBs4Kev- zG7-~4lLN(_sV)09tT{24kLy8Z7z;Qb;l+N;T4dv$MV`G1le;8sGsJv}E*nRAxgol_ zVj2AgAl#kJ2A|KnL5n%gM^kW^Ex_Y1O%6UrFU5SrN#J+vfXlEjmpl~7Wc_AJPeABj z#je|;rTB3AQG8D&BwXrqX%*4XSi<&mBnE#Y3-+Sz0b$6-op`mgQk!!F8RqjA!-gn} zUp<4OC8e=O2v3D0hM85nJ!1qeP8qGZCYG-WV6AB|u|$-fU+L0Y3efgkwcJT`4Z)xx>!l!(g$_2yS=~iU~;%t2Q!cB*@ zj4`@pePpM>-2wgy7y8a{hM$;N3R83dS?(CZBfT7KDd3~1lv+7NWLKds@Tn`zWh;C@Rw(~Mx1nqgJtI%F@QZLu~9DOY4- zit1VuB>X3olLVxbc9DlCg|n?XG9yU~X08x{>`a(TD}lVU;V9_d#Y#9QiI?~d`D6#Q zl$NOY@uh;ewqK!7F-{MgfVptDE~XIv&B?5$OMXiRl!79|*b=FrX=-RKfK>}=-RaIC zML~J<;tzIS=ARetC->)m;*TSn`hpu%c+HGT*;VS;&5T`qfFU{TKqXhy#XRm>Js#%D0OR9oALk5(2)*zi`CkmYjnZ@AW~}x4ay(1&!$yS~ z`*s@WhH*#XC=Hy>2Wa}P=8k8JC2%iyy5G5RaAXK-zDaGWwL-hW{;r*^)W>;?bq})W z%pm?gzbVcRn7v|dCN2;iAUs)fe7oi4HRB74HWyj4OzPst;;2Y<+&ws|!1pZ_ZRpk= zoJ{P;#+f?*r7keyz_JbRbZxYax6pl8PoOE_`L|=NK5U@T1$O5LvMp;k+6j>IQg*kH zlFDo$1qA(!$0N*jjzL z#B%-0rWAfEo#bpjxEloP(Wz0_ST6Nxz*lk{(sziaG$jf5=Q-YFL@6(=lvxlQfX!Ba zEyn2hgUkI@w0v(=`bO+SX+rzVOMdKi%1c_xKu5)viNV+$!Og z8`#_Ls93S>Xo}sqK}(34pfa}8BTLu{ac@$#3O3cfuVx-DQ{DBXEXMy5@DzE(JWhop zR;OYjr588)117BeZ$2xJ4H{i!cMdLxIPC6Amj#&M%L#c`?eYyp`XugbBBpB}OAyM^ z2sqfv5;nkADOABP9I5_(T%>U}|yFuU0r1f=KbmdS6(p#o2NQg>ph}c^ECM z1Jqeg!QUQxWb39cU0hxD?KTlsu_-7#O}5s1?}bu~@!F|sU>~7IfZ(^~MSo4WH)Zrf z3I`M+c_;~4l`z1PT+F%)K9CEk{4^8@19y8R^Fs@2M}>KDq|o>!-a&((9~iIR;2hZ4 zNIGtTeusZRs+YilU^btRzU7h@*H(}rA^hHjgl0^3JiK111QqVFp~laD*ZuG99Y{(S zDa{H`mXib#Ld;r|k=cEc5^w{ERQ|-sm(}rVc7Do}6(ps%eVA)dc*s4aa{5`1@3#!D zf$hs6itJUdT6m}A@X-_={JCo&v;4xcrKQP=)80Ye1j=6Ep z?+;=<3KNvyCmB6r!^3Nt%CpTyM{sDeo2+pLHsq za;ce|ShQR}Fg_k0@a+hkb6TRMf>F?8flvW_=L!JP=JKBG>ZG;6y(t`SZLr$jDPx>D z9ttJ8Tq_$YjQJgmcS54a1j>%A>}v|!=$dVp(=bx7v; z(4&sPc0@|kmkPXIgH@__sFyY~G#!B|geN}~hSZ{p5J4dNJ;kx@7Sx2kHw9Fu>`JoO zCZ@vkRCbx;NMe%i=N%vQQ-3FSXFg{&hpx-+be9^(m-5=W%{;Q}58nLt&21wuXjYLj zvy1ZV-v$#5~N6qMI%Yd(r=}qe*6@iUzNbkgD*bTgE-7!L3K2q2(*ys-Y);QP% zL+0w+B&FSiLS>Bw7y0$63v*=kDDmFA?ixrGCt2pWI47q<9f@$BUv`pk|arNQpo&_2KMyS`Fpvh~SBo;+>tHfrt9M0Vlu}EY*GX2fo zj77C@NN=J1Tr;e+j&J93NBR`1C_qd6=5Y4Q{!lXR_lA4%GX8l`ou#^fLVsAyi8Min zf)`(gP016&^~&Ii)*QHjJ=7$MgXMA5t=fSrOmi>LvEy01%DbMhwQ8q^H<3@wU&xx&20)E;jkQ z1!UCZMC|1`FP3l%N1fwxSUU$~v^S{0+eN7j8V4mCvd4kicvC|k%P^p3hL4*A2NU4I zNHy)mfUwhOoIwfiCQcjs-F(fIYH6n8Brkfhe9GmMa^L;r8T__Sz1nGp=Ny@*Ej%`+ z*wblsOSDvRjp8Nl>o96jMC8}&K)_mvYmQa>8owN*Y@__wGuC8SvECy(30v}8gq0Mr*kPdC0 z11{b**~$!1D6h`Z@ov;|kB{9uLlXzHmU)xrF1XfoQE#i4M*AO{a2`-gL_Z!;KTmKH z6}_AB@?^nzYJVjE>cyeM+*+-~LR#>cg zgH&EMK03{E-afZTVkxX9{w~)MaJcnjJWQUq=Ri9>1EGIvIT>r?T`7pR;7UN7=6_%) z(yMHcCE~yZdmI`W6Q}3kh~NEX?`kE!)M6nP>|mf((;`PN;wbJfX$k|D@k>0(h%Z#XhI{sFIX~Z zFs=(S-2F-F;OWG(y*ss9o@AS^^poIkU?Yn%i|}oBnQz*(UYl?8Sg!N=b9N%C#k`VX zPUYSb3gzP^P*RD{Rd`A@g6)C%z}|+MK)n4OkIy2P|MjdnyTgw)LiX^c#CDRpSXt!m z;zrQ~QG1x)s9af|o!dEkJUC6$g%$0X$cnZ2<|9Nu>+wPmga-s9Vai`cY#L%a)7}=9 zSTuxq7+!erFADNR9l3C5CiuN?5hT_HFG#?2Ni1zuJ^$ms3g!O*_F6C(gYD&c3;3v2 zfr9psIrBdS$(5sTj)OPRrM12}s4Zdmodo?=v}}R{rL$Rq;JvfJ^)+0*du~Bp=A9E! z!=ZFT^Z@H7U?^v*nqMvbGAn|YX7blcweg0s5%c^!0z4x}jKR>iIQvtYmvDNp3CHoM z)K*gO9E7*{zIiw0DNdaU-XyF9eLk2g{dhDJfAfv|-=o8k@o^a`p6VY6{V)84J7`&A zu4%XFU%TBu43Z^=s=wJGhDk`7@t5+zAGrMhrpm7J|5N+_>E^(Hs`Een`TynDfq%{W z|C@3C4`>7Y-!uImwgLX~g#XdO+7(R|clP#Cl98=s%iaC_8w&p&0VRweJRehzQh8;u zQUCP6Kd;C9!$})Z^y~4oB&&J;ij+Tg+VW$+JydTDE+p;`I{Ob@|L9~z-qrJGsQ&xe z?*vq|2d>^!xd+iV^k0au-{0R{Q&l{P`scX+;9q`+iSjp4x)rr+(5)S&f0x7m%mr0; zwzSmF|3z*I7YN2#sT+v>)!)+HZ(s5$|1G#N0j>4F$em65P4vyS`Ea`ZXG{WJs=oy< ze+gUtPr1GpvcKs!3tho9-v6|OKJ9P8*5v(X|0!3Hjk2r9i;a;;mf&ybH6m;e%G^nbd*!k6E7P5-xM;9?N&{;18d?=)KYOX9GqiOZgBdT?J8 zxRKOUQA`F_a9>0l4_qlqN8U?+`cHb?*CIIPYTE%5tHn&e9-v1Wk2^kuw>~nXv@d0p zrWtmal9D`I`|XMurCK2B$|^pIH!Uh$Sm;Y)Ou0V!gEnnK`K%jr>nyFB;qLJ40&Low*R8$Ggh9Pr%gI--d7`Sw zvl&>no%p%gJ=1E1zLxD| zD*`_m2jC|haK-c7@;U1z6(=|Z!W=YN?~qo?=LUx+}hxAwF2(3_mFmVwZv$*PJO6R#OLom>3UJ3 z*BHIk7q8J%ON)@5_oI^ZXlWB-TTbnEr#s?Y7k2HYOd&p3TaCoJxQ3%Gd@U)FdtRCG zR?+zfw7vbglywZKc4DUGJww9SKI`g^Hw=9R5F;QHpXef1BGP$s4;!%w<>0`kb1sGw z9f9)qBPTPC%TD!&O9|H%(V8<+EEfzfes3u`@rotgyR!o_{B%6+Gb+yKSXzk@Ip3#_ zxm%tu7WMba2~K_^#Y>p){*qZ!&$r`olb#Ey$y1R*>kS>jMZcahyiHCN%koUxmg|hM z=3d5^l6mA*#;!G1GnCXQV>@{Hh&sv7GJryUUL}q3op#52g;jg#=sLShrwdB5n2qQ0 zn)@k`MY4m;254sE5h0OLouGqRIAnf1BFJf5SRKl}gFyM6eEr+jO7rvJq}EDX8k^0& zpKJ?1@*=?2d%R&eNVE|+43D&}B+(RiR6IFccumS9SboTIC}OBkr!qOcfS!ae`)Vw`1k0sIFF1NtIG{A@SFoS?Zq`=0caqGM#9*~Yh>7jK z1d2=d53ddnw(*;1jnDZN$6fM${Eh!@Uy+cJukBmp3CM@IUujhtl4*7=^hX{+UU6wf#23A_e%Mv2M28tqsq z2r^cma`KX9)Fmu7cD6yNlVsevF`kg0sQjM7MzEc0JEK zV=c40WKpNHR*_x}-%@oMgR$tu zW%%N>0*hXXMmV+e`Gl`?Z^kpuBF7cfB7Nm)`PZd8Qb64d0WiqmK(_;i*1P9oYOuJc zUY1cQ7k*ir6D^vife>z^NsCB(FMTXxvf?6UJ)MYO%5Zsar;yqyd*} zaCTX*E~|Q!W^dD<01J~tKV7ec?CRo_7O0_x?hJ6x>SHLl%5@9T=(+tY@^c;&(xM%v zc0aQWk(KU_0*osaI!uWb)*}+-m>1Q*Qmx4-g=z?$SVR;yxaAiWswPWJ$UP{pmL-MA zCs0plr(;C(?%H?HT+Xo5&(-g5BG+E%XcyR7q*kMJHaCnYTi4aNp@cFEnZSX@E?@um+XX@^Pxw7AnO4jc!K#UiWnKduG%_o$E^8OG3%Fm_+>W`#t)c z?AIiwZ%b57`KJSZzzY=&eM!7o+b0j0$e>^%LkIAu7D{4=H?%|1xcYOJQQZ!Z1AYt^ zQ$SYCGLQJE_wdGn3FPa3*v~Po9+^DO^^d0n$004hFRU?{$(7~Cf_*x@HHW?)>Ioue zsTQUMAuCG@b<=x==YW0R4I!qIUJBzv-fIreiund^De>u?uZg1{nTBo0vcXb(AOFV& zlVnSD(y`E{n^gh-@F1pc&-JO~!8)xAjWU2KhhnO)AC2K7EbN&7xiZ?ech;6VD)J*(-m;#%n2f{6~4K!YaBylwxzeb?~3v+i`GWUY2<0!vM z91Hb{j1N2nR>wZmnj(-KpE&u`t^8~*m*li>@9UBUESmYUpr$nWsY0}pVyg@eK}B>e zZ0|It282viduz1M5{gj+LraHx49^(s$7 zF`3lRq+nKI#Zd=3?MlarkBU|mX*5OOAUpDZNuvbphcp!JB8?`0o{*>&!V5_YW;4k@ zX4YOw6{`(#;GqxB7ebj0JQgLdTC32ov7DXEBDzLPoXCh0#g*-@^g_~(EKO6Wp*1Y( z$cSV$WHQ}WC3(tQdJd2ra*fKxU1fA zF-Z5QT~pvhn$UWW;L}o}ezQ(0CQ@AF#V+i|@%Wl#QrXsG_@-sxNQ*&O8Py-f_%35h zw{Z&r_{l-Il_r9$^qSKOr}#`PgqBj z*)jMvB$aL7NQdwe+?}a3Eli0an;{3uuur8sVjhonMTeY8(EeUKbJ$dx_)`uP>- z>&w9-QVDToB)%NVuEPmuhJOQECC%%HhS<^&CcelXl#dQ4KhA-n3f#oEd;B_du)$u) z+@TjH^*TN0ACkF0dd^4Ry`v34NaiPUfemeGy=KTEB?)M+LIhd(w4j)G^$?iP3}kgD zalAMsH;y3>SmBi5Qg^2@e$*q;n|>DXiUNAFzjTGGDX!RXHYr%{%*!HNkrgrra!l`k z8EuAcPDCdaum%Gk&-~=pA8?#x2-){PNGD&jr^)g~WWMIzg?Waig5WZ3Y)(6G@o}AKO&G^tCQ*lQut%TETZr;b1ccY9CdIfV`8~kx z0FxQ)lu*Z4F(=t$NuTp%aMK0tKkX>lG^0NsQ$$f|dT;UeXw{={g-e2aOQi%TcV80< z6`czoC(1e(tEW-Yw%*C;_W6<}rA+owOrU8@_|AAROnZn2^Qfn2JmEZD{X8gq_Tzh2 zXh{zaV{*Y={4w;JI#C#MEiU#mRp_(?AHbIWPKc~GJ>9aE^lZ`FYCj@u<~o=TaHpR% ziSBYGj`;ZTb$DXgAo{^b+k$yEv~~%6cA>CJsEC4cvGv)BANvW*PlbX)_h&9BlzIOK z7qtdcnnTzE7$cV}s%YJh%P(eNE@D1N0pHa}v=Hu0c(h(G-_&+aOdOS6Ua%XPA_B4R z^8^qz(6N5p<{hyWV|s^Nnk5I~gN1K9rNE!Hg|N0{T)^KtZRm(f^~@TM9m#(M&Mp8e zD;u(X2`9+}=K>a+p>Unim;5yD-2+2*JOad}%8HJ`%5_6ESlD7Zq@%v2X_ztN#TFB| z>;iPXj+H%_K)F9%MAfF%RPDZ-exOXhc@h3+9{0yqnn;ZRejF&TRjRwudM+68epWPot zHg|S+48N|CsU*v4i}<|L@6l$A)gCVUsCOKPfcd@rmPRf`K>hu=zZx&xf(y{;IDR@R z9miqAq7+f~F!+59nN#=Wo>FxI482O-Wu@D&X1nk7$t2mG_YQ+OBxf;t#S8Rm$Cb#g=K08r{r&BGFb<7HIn3RI4@e=uJPsFsz zI3v32;}><(8#)&A3C07JUT=W~RCb{T2K*a&KUI&?km_^nXh1qY@;pKRPU|tiD)U~i zSsi(=YTV#`QX8gR^#^-LAEgvzd&dt`OIhbqZ+)^mXecWNZe?DZy_L!8U33-8njTe1 zVilNSDkE3NoDOZLi$anHjPI6oixufZw(uJ{%DKPxkF_TstXcQN)RqEAu_d(@v!T3@ z2oVq-8&rW}ye{#Mk&v#|BaIWUT(T|O@~aN&jxV9+ymji7%Fu3_8$#O1Mntirggpi+ zo6&OcB84Hl;rGa!db=eCGI z*^}8hrnWQy(6o|J4>%ZqoG_+JVuq}>m}{S!Ld~t%t=X|>g2Mt6q;8KO?=1J8 z)bPCou)UVuppNV)s2V=*ZPrjZEl-PuFmXxR7`}j2XwfFQIEeurMQn<L@;7!Jhd~+^j_W&X;2B3qvn_tf8ur+r^A5_eznDp{N$}g z8x+_$`Oe`ai?)*ED(_iNk>x$}QQpB=3oqP-h}l}bc8p)aNP?phd%e$WiGT| zLd5PVXJ}1FJP&8pa46(dWxV*9&hy1pmGdvORg{N(sD%lEr3LA>8o2ZyyokYFJs}^z zDHP{9h*q)?wCPlj9*_(AhlLDP1swVMpri`T3fX7ruE_lq)E~16WX)q#@pYf1KgVWX z{$ynJ=_Xy^TWV{^9IW#$H9^@J+0~qls}?ujbPDPOk&2;CGgr|RTDG*2sMZG@h>`L& z5dFkyU@7Rpo*x`m^yCi*`}g;IxI_(EIYU+U@@k1Ok;V4XXDbeeIzoi^kQcmxgJRkwj?GO6tc$^Im7%C&uCX zo)Fz=Nn-E&JG)kjVKca`OLC267oTC(sBZ}%U1cx;&f~3`TerncJKuvj>$f&OuvkC? zhF}gsDPByNDhZ5yxbbDomcH5vx|A!lomZwgC$AEo9&AXvO~6&kE=c7HgF5&1O|1@p zY7=GqGk^a!&umx2%ZXY|>65z!9nB!SsOnN$lWg(C&fp28X^ws#beRt@_LUu+pT$n`xrsp5>~fY@>+gB14?%f? z%UDXk(T3MgnecT%GPAkkeYYUrW%3?U%8^T`4%LfDThXr7+-ajcBPj zIi@ZVL&Wc_;v&%pZwD@X&ac`PQRNm82-=s1#f+~MK(Q66u-^ORszp!oQ?x?Xi+Q3XK=LzZuYF!QZL|1KuRlAjO$2O2KM0(lND)QqPFOO5$XZiX<^{7A^rl8YAQ5zBEWF1P;+PxKH1Oz# zjH0?6W7zg4FrL`B>-!hpi0t#x__|TvXcE>cA^oq^QP-c6Yx;cMb_Z0wOI(#CGYWq6 zHRqv_$i_5Aac1Fu@VT?Wm83E&whgHLIaNu=8NqRr)xw}Bimq{z94aJ{8MVNIjbmqQ zZ_G2O)5xNN6>(QHjoY)=(&*>YuMG%Lyi4wVhh@5=<#r*D8^Oi8r@yVo zM5h%8zqMGyP`>Q2B$X1SmuP6Y;40l8sO9K~G8CZLRpMl- zpGRbR6MUkkEW@EM#tjXN^_x{S!H+KziJ6xv+3l&~-nq2B=Z2_T#Z3sZyiu6N&88m- z<0=?0Ef=sRMtcOTQUo}&vD4fH-5$Y7&@a~&vp6~3y2XofebUtZm|6Ss5i@GmKi$9J zH`ND8Nx94rZKQJ_ZLW*D?56>;kM4VsyDdtaBlCX2ms&0oKDuci-ncUzPH8gx_QxU$upmZYD>A@>eedU+;PH{RQFXr^J`k7xz>7UnljqmEB ztgdT!%o;BL<(1tTqVZt+sDI_C@?MkPQBMc$_~((pb3Gn^0;9wrbw579$=;>TVwKUo zhrw*nR_Hopl!r@&vxcoYUY!nXiN7TM^z9-(8Gt|5>;J{O+rq ztI=u5@g5+fl$5!oB8&|vuKcW(9-9wNtspMJzr0*Yv3454m^rcEjj=Q6eV#NdR^?X6O#nBXXH2`K;tJ z(I=@cI?LvRPbS*9^<`?zG3qLeR~>dTh0B|F*s+}1Oic#tDH_XwxDWHS(ZMlWK9n~B zw3MaAbit=^mDFY(;_J9cMv?q%))0Xfn@)KYeq)?I)u}Is9y*39f~kH zis^|W9DBtTyIhJqQcbpFe7>hiwK_7`7y4r*P?RC{&QXTC^9Nq=h3c#XXZ8>BR zzbq!Y-9-k#?_p&GGk|dhS3$;Tx%VGj+AP^-Bt#NMs=>nWy6rd>C`G0vCK| zO!=MF@{vQAh&fJ-@+Bmi6Im>q5Q%aJ_%s#Hw4OQ&)MVk}XW^ z?7r}$!yxf)T-{|=CWn9X9txyaL5*A!b+>JMH%?#ku5uI{Us9WGzZDjUKfEBYwXJ!`M&W0CzL4vP`kGlf<&-a6 z*iKLVf-z`Rn)&7AK;J};5@X_=g;`^pI@oREoS}3{GKbs{Gq|sbDF7qRxwaP?5)5+l z6##Y1>w04AV+#)j$pz&RIHG;hOf%v#?RZoJ-_1d0C#;XYr*gnoT4r;km=#s z^qg~b9lIdfP;A(~tW-o%vO2CSe*Z*uD_ggRTBdEX>dlKu)x$^srBIod=-`QS99L5f z3YOSDAKMv=Bw00UllQQXR6MA>3mY|*TEJXy0M038J$D#tq+avvg1kuFwrpIhqbi+! zH>NZBEbDB&Gd*ss*igX2G~M3quBq#fsL-ztbwP{4vQ~u5he@#=}N%0QgN8PbBx5+ZoAa3I2VRD{S380V} zc&ij&r5?5I_{jt|dZ}gVTSR6I=UYm1U@v_3P#a*O?eUvCPmWQP^0>)jU`vDSob!?{ zH7zNvo_Cx%7Zge%qE%8tjzlfptSRTCOV%9HnthM*BxCUzDCBW^Fu!W3ym;BxZZQ@! zx^NV#hvBB4sno~oUw>s7GQiBN4lup4z+;r;wrmY2{ph&D-L*!+D>b!}_RRu^MI^`c znxTd$QYCt7y-KXmLea8sG79&&tQs06!Z9e)uw7sEbg|;b`fdV1FrTQ z!_LB;q!t@xT3v|fTxo)kG*7j2{NS4Y7-R17On$YfSi&BhTG@Xy;B%B=%Adl(zD&b( zt6s5eGrw(Ou%I>vT`;tBTiZjgtjayV&{E`a?)ZhAnEr^zem%|oqNM9bRm*xwIizMU zbkmbdO@Fou?iM^}Xsli|I6!UJIT@?TwDj@1Cso7!?Z{AsSoB3IQ}#_)73xU8Xc;!< zAyi=$9vrpc`iz9~`s2h%fW%4UY>}|9_9A1cPMm&5G(2ykvIXZ)pWS78UPrU{qV==s zGTP-$uN|*nO4_SNX7VELui?Dt7Qs z{Woq!gH$U%sIX`nY?bz?-Ngy4xaRf+W?&Dk(QWOrylR$tq)kaGqK7@;#FWr*`gF9R zg(ivL;&W{nrE@$WHkAgBg+0Fo6A+>t{n_mp&x0L@H*2Jp-4WPI(HnowHA(uqT6%*2 zbVE$amqK)yR(SV%GDSq7qH4(Zr@@%7eJ|=S}wD8 z9h%Y+6DzqRTDykMeQRnqSB-Ikiy4S9L#+KNu1KxQB>Xn!suM0G<^Ar1*5q!9&Zy7o zbNjRdpe`ipfG?qauA2LF{LGHTX~&{b4Btcb1>f*`M-DiA_3j6C z+q1Y7n#fL3bIi2}zBSx^e(Qnla7z`JyE6eC-k^HuPA)*%-;1hVQt0m3k20rt5%v^# zbH>;xPmO-_T=^PdjRAoXkH_v=wROn_tQZWuCOdDx z1P^2ldR76DaaXVt&ZRtS6^kje1%=PUbq(+e?ewI|^}mtsvCXvbTt4~AWxJBAa$Bni zYKzH=UoG%POkL)fcce~WYMz+uFPt>ClqdkbR|73FYELC^GV}6JWW7%IHcC7O z4pGg|8!lm?T5Xk(NSY0wq)TdZY?akcEh0>ZXV$qEPszW@4M!hkWEEcfpg41|%NN%q z=~>)1GM&C@%)ORUw>4xthjP>>th+tp3=v$9~>Sy;hi zd#%1-UDcrxOvz*OkR(40RxkXeM1a|5iJG-8PjeUr?dj{udBgT{_4TCY);-Vtkc$W) zS>l~8LDG@lt!4D+DjzTZ(yQ(`)#@zyR4u=-e9D@iV`ccX+bBG9V!3YT@TC=Yy(D5W zb`RH8*}h)fHs6o2W*DMi=GMkb|#(+?JQt<`0$-bSJHC{%Y!S?c;cDr z^rU@Xcu9@*ThcAp<69H6#EGgEZ;K%yW+&~fZIr9IX-e?uDW*OnN&aFc9UI|l=DU0a zg+fY5!GIB+tU38kJ}~zm2eL3wLX%|KY69im8D?fK4=8SBLK<02F6EtK@xhyY3-<(s zI7^ucw*5vLqLlW{lf{lCU&r}3C%*E{2)0%!H9OdV3ygl@v4F(5EubrYn>E7<96Dpc zl*G;Jnfj|Y7nN;L)rp&`Z2!(rPKZ4NjK-Z2pWi67`TNp*8_4yJv)ERH^~~2=a`#6L zne;Ig<1*q9Hf%$NYA%9y?wm$HeC4tL%(LnS^5$^B}M~b3_oYRz=9;=y3Y^5K9 z-c)dh03WXgBM>Wj*aFK^Tsox&*H7DTRmU}DJ9)i<*X(yNzMT%BXyV0QEhx+-so&@~ zbhfZFFYMU=%F;DKk;p#t@|mS>=L)mBR@%lvsA9qZw-8{7*2)sx&r-A!kv_LVNIn;| zQbF0=&73HY(!i{m0;#ZpQ@b$z4l8}lGIT)Vt8O%hMbPv7bKPEu#n8Q66$G_zpQFyK zrO4na(_r#qE3u%QuOH&Bi>c<~2U8cO6MPO?xE_dp>D~uo2lWnjdCl%RtfZA!>@-W( zx|U8_NE!hAoM$x_Pg@4gx-0UuloG{Pf0$OfA=3RiBeSo#{$ktFjb=JSjpoCnw9rqM zm_Z&h?6r#zk1iN7@u-_I5Ta>{&}#m{tC?IRMySPPQ4pe@e;jF2Fm`QIHuF)ml!Rc& zAOAhRu@HXRXc_ukNq`LeJ`09yr2d-eST&Y5JMtAw*uTf8M5Hw96@$DlNPnDs1ye-> zhIvyw5IZjRpIZ(=kJec-P8R+61BWNX*VtFV57S!`dqkgJ|2{?yAONGqS_~5NVu7z+3=efBV*Skzmhr2m7?To{BdG2KAucOamkMjIeN zq0nfGll}+YZ>Im>i3a`y@Ba$Zo+b8Ot|gcI`zG(v&S za(^u_dN&8~pPfzdAoep(_qR5E!+2UuEhrC69 zlt&dDZwhsmwk5b=eV=R`-M@;o!+wo3PA!6Q7aSUT!q&V9uZsEMgD<1u@ND&=n;3b? zOVLPbN1-uC47vsvH#_LR2Ifgv^FVZ ze$I^b5AA(=AOQ)|{f1UmSSnT%3GL9?JQbRZu)tmcTDfJHAxd|0yP%A0Rox zPtKW9{P((}_{szI5j4fkVSZkuQPrLHKlaAZV34L29VyO3*Lp+sJwtgP_f0<^d>o0M zJgPFAVt2FOb^S>8#@$lVQ;{&2SdDa2qixNL^m9Hee`pIUK@}H(;1|W>ryiJkeYoCW z)+oljE;+2sW}E+}UEP(ConFp7zWkEu+U<;48qoa6m4O7PBV1yI1@*pJArcs~K3sNv zmHT#BqF%bL#*)DTZx(8LMl61FX0na93F>}jbdFy-f1{AlaBU?YD#Co78lz6&Hz(52 zOmt#E(xy(8k7sHLgeBFN*IiAaV)1^;a+XZlQ#vyRJ7wE>HnUiKY~RJ_u&xgeoDOr5 za=3~s^V#kYn(By9pvBq`Oy#Y+sxex+D05gs;%+#)*HQ_9L^9sjrL9k71SS`!-A&QG zB~2eDQd2`xlme^mF`X2%l(ZL`=4PpGOtdorw2h~phfJ~uYA!C0xGoNfv6Wrx8OO304xwd5PpcA{ww z+|b|kqYVtKVT;Xkoq-~YwwuUw+HCE*nT*A-gd-riKpTOG>lBuY!Go$B2zi1#uIHId z%Dk#+cqQeK`^s5q^fe!zuTdnrP(_O^5QNA1VT1I_Pa~Lh^>mEu{dV8PYw8S0iYqQ8 zZTbXhda!5DGd2wtjBD*BU)_Cq8SVYLHJupBHADm67yNe>%4N3kQ)LJTfn&zT=#D3^ zn9<7cY$r6@j58gKVi2OVfib*g0UvU-N2wA`BQQIs2$De zpU>_KZwJswx9U(VTG+?OKTr!0w>%u9f5!C-Xu@&Q?sM79hjH$WV@2aWj?3Fu53fZt z)d0|-M#xG4vM`qAZ{@Ad5WGjoWSL`f7F!~_ZVRP!Sr@gvD#`x4<+NJu0xb81*<6>J zirHJj$2Ha{r)^~_vX^LeLM$>C&#e5HQ+doO-TQZV#&ADy{921rAu20}Itf`elqaS| z=V|oxQo1vs8fIB9CovzLdq(J0qfN!-I1>J%8u(i=34ES}H$$HK!Vah7s0^SxM=J0a+uvSFxz$?z4<@S^rh=e34$%gx>H`*L# zp$8CIXu><_?*xLW`ezdKV4sKJcEOCPj#;nE106INW6JxVc|>l^I|E;?sG>}QdSetUlTVQsuz+P;cYlQ)e5ysjN8BF!;3I56}_lRB1B&$bPC%R7=P>6CG zL3mQm!XxOW^oP-TVLUg4QuTOYWx!7CK3S`S-czgl81iN>IigYUI_~d6$rK1Go(5|S z`tW-pt`XHwnW;Zysd#o9Z;anqcJzMx5wnBO$6^E9!fwVP{%IbNhdu2-7|~I&VTdS= zZGZVnEH+@SO47F7_SpjoA0J;-6gRZGw8=YG<4Z_83Ga?;t@|%vqQ?>HVN2B zi8~f;wnNXgVqrC~NKbegaU0s&d=0r+uut}Uhv%jrlVy$~tTo+)cDf#?aebEJmemM0 zLX)Cr(}^Sm3$HzJj~e&ngeknt4T|8;ZU$#Ekdc|>3VX=?| zr=v;HRkp+uT$x4+<@w)*iIE{_siLXm_hx+?A0Pp*JGFJII7+1pwO?T@b0zdIcOQ(j zxHw(6#On}XoX(^;{t%S zGK|#aMwj?K`3!@mV@Kf}f8drzOh7Vo#(DN~n}xb|JtFgK&9zVF18=|#+DON&cHa-# zi1#zG=9W6wmOJYz(NY={T;hG6Iz#5_QdGkp;3$TJoe5I$v-Wl9tnj!+edXl)b~6n7 z8sE~=KH6uP*_G-ox*QbmMknbp;wh1Ae6?V@gl6Q%u%eg|I|>960U_j(AzFEOpR*N z^>KZ<=6v*|iSd}LmWXfFLJ3;Yo$)wgbNDRtfNIrGkuHPn$L+x z;rRekK_f8I&toWv@XmZyF{5E~c1JE$`bn2rm%96hb(*O z=f}{Tv!iwS-WuV~!)X)-r-Zu3rtd2<0Q#}Cz*M~Qc(`hK)f{ZuYIbbdYQCS9dM@2% zgFfQ@&p5QNi#|A-O;_>3e3FhObzGl#-Aw>`D;2V>7d+m`1HcHzeBaQ*3s-h}V{yK2vxex|mA{d^4gK z9hvw)b|;|_;>@1p6H|W{a)t=xNVa`En6^yel$h=x>sS(q#ICWK4Khr3*o z{Js8Wl4@fpS+G;uE+a+!uIjrSz#4j7DadKlCyN}MO=_h;l7P$20lU)^!k>%B%Ua)0 zl&0xOe}Q03K;iHNgMbsI8y~b!rhxtU3X7-C^Zd|Je+c!4U#m79J+Tbtq+f3ujg;q` zDwJkuXXADLmEXHY)cl*Lce=XMt4jd?$BQn~v!miAYdMmUhIieV6aDl7gMOo?hg^eh zP7o}8UvE(#lyt`zL0aN^7ynY|E30eqt(1NxySFeoPo@dZt6aP;Mtdj{K{q&{SeZkk>4xRw(ti^HmLP`1jhR9ABgdy`@O`Z z;-vy3ektu)+WmBZPtk`;nE1x`g{)w@hEMRU0XyO&594%f-B~}-ukqJeba@m&ubBq8;68DVBHF~0ewXa+@T1pMSbWW>0JsSW9gSh&Hb_WJEY4c^y)rxs!tZjJ{_zFs`<#lz53VC%_e8RT& zKY)-jdSCNN`SD;5ZUX+zkW~hL^DaoWyaFLDQ3(FmSkpAMxM%^{&!2Ly8kO~_X?~>0 z!4hQQ1e;pR1aQ_%Cv*5bKSg7`rzU$caWxCET%P_BFfJG6ztYT8!Kg>qB1?w}M@Acs zYWZ`Ego;6Ky3Xwt6aZuGsME<>r6Jb zK_sIegeT*uJi*Op!uksuDu2h^+`<1nL93C2tx%%2o&TjP;CD$*;q&ZnHoOF{{B_OPCW-8$^Fxe{@n&Ty#e+2DPste-~WO9S0i5%2b%mTg+;-Bix$*;tzv*movSeK zXV~9)0^Oue=QR^>T#Akl`EP6a>w~8u;%*sj`T4uY$qZtc$4ydRJ~cHpiGWL&FTBsU zgy$C*%`G@)|HAQf1$DC4Aj^IGKfVOm3BjjY?2~<3lRsu15GrYqr{Eai3-dpKz~6qz zs`@X4%7E-2N&+FY5(iNt&m{RDMt)s+W4ZA@aQ=5O{{>qf)Ji62Xutl=G$?Y7^&-MS8<^OMzfB9cl;-7Yyvb~7)3=JL2K2+BK8T?T&==zDc zM=PzZLr9MQ1q5P?y668e55PZ(_#-)@pgdUd7fS&+oDPHAdG3eZJv|G|n*+413Za(u zTOS&oaQ`ICi5Dzn(G|d}fcz6UQ5y1UFSiT?`T(r;VPfX(^CILfe0q6uVwxgs2vRb;rmSDqxGB7q-k_bARwM|ya6Iy{Di-e(>LLXzF&)E+l+WwTzPy)dVS;>WBu?vrb2pxK9e3ta z|L4vSqHR*Y#@`ae)co8&ZF1L44{Tce)mQUkqv^y1c`pXrPF(j77rD!ZJnMgow+$8s zM2?@`S97Usli7zifg`T`RRxObrUwR9`KxAN{pRrXe-61-_g^4IQ}_5AR_}jmh&JGV zz%S<2E3M|Zba__~bV96JHl8K73ut|Y`$Mze20(-CxuUF^A77o0pyR4w^R<=7Yc-JQXBaKI7HHG7x2R_4=mOD7p;YGAz=v-ZdmHx3kg`m`_AABt~# z_*ER*crGr+8oFkx%*DI5(XSmUE|=)mmHX$f)u(nbrSqygJHdamc*^oR>GJ_Tw~}i< z`U;wwcW5fYej%koasRMK=!y7d-7=ai!0Rs}m?i^jsv2-889Bk-u{u(j?sF%H{ku?9 zQG+Genhz`(n_qRmRU>QEiq){)do3cZk{dFFVb!+rxNgkVlA&57gm zPm%h?_A2*Z0?^#+DD9E~T+A0tbPs${rfU={S&%;h{}!)rF{GbTA7Tvmxse4p(?h>3 zyd-`vnYk2f#lKlU(@*uZH67Bb`=i#K;Df5L>wN(#Q}c(`ktc|~{G0R}=5pw|TksIv z7qn;h0*D%~`{K{^LVuR&s@EV}sOq(XB{IhR5ii{Vz{bnodlj}#&I_-Ki&bSf)Zp%XVS39W&~HB7wlB` zYxZlmas#T}yiQSx@cW3*RUHp9fM4Oo^cEVtPrNwOj&L9sD`B33Zw|0?x*1LL$CAB%I6~ z+o0@dLmD6~hTEt75x5(sz8$r(o%u+)e}0`|w}q*;=-i-va(f?54%zNfE@PPV7k7F%<4T;6;hy|JLe{-B#X*ut9Hc6q5*R7VNd zc_=r3{b|Q?zc6uQbf5NVs%sStv95N_eI|qMHksl|Ysv$g zsIs5zhcOH{H& z&7ipAR^O}F`htoEZvvzk!XdmjCBLUq2tDjrB(n&qonXRR(LdefsVJmvHF86?!(5x& zYXUQhU*r;tbkpJ$#jt7Tw3qlW#cH?X=&a8JnF~_O4aA;=VKXnseh>|Eg8A& z*`a3Qjf+I3+g123GG6e{gRP#qORg;)4S0AD)qsEw+dQNV4MwZxY@yg(!=1XsEG-fj zV&MU$AWFrcxwP@MkV)cC-BeWf2e^ZHF*>@BTWh-Xhu`|@NZG}y?W>>nTth|l&qO~` z^aH}J)8pN4oDd2>R3%eAys|Rhy=zrxOi|5MGEEMk3;|jbvlE`6>$uy$s_zi(vR(_^ z3V^Q}HuNj-)}il+nEP2@w91lpH`7vBojE3GpVaQ_+c{HxIYu8&t6LGI-<;X#sBs-N z7A(S4W-!QDL|5q|9Qzi&uawNfpmsRD^911Rl(_aClMCo(&PqM0PwFOaY9yya&qcz& z$eH4aBDArlA)@(}MkEp)K)9$U*}x}#r=gdt*_p&5`#4%7e*E{7x+;O!b^~W&(pRv- z0w)$qof@)~j|pr?xpgJO!@Z}7(W*khH2&O*^;66`%=;XfiP&%$DM6p*&1GwW#q5x6 z-MbYl{)#xSv(46WQTPahAYlZAI{m3YZOay7`h!`H1+qn^PoOVEzR7GxM5%#_3F%*)J_R`u>(ddSc51+M zGfYz47$Fz19#0Ruc7Bm>0fQa^Qgm6rmTpZxH!9|ugP}cK36V}UxEn}{Or|g~=QUL9;VDwV|Ok&dFbl>!6xeQvd%>~Y~ zA4;*BBe?vp#ai3Jula73hUx_$FJNvH4{1)jdqIyQZ=TQ{1r?SUvl;klF~p#t>Pmho z9F$OmZSaw@XMgN@Xt>xUWfJOsAt)wRcP9rDeSZTS?~3s~Wo>d*zufgZkuoM~I7-Lu z&ahS#*m2*4^Lwp+lE~=%?E#VP{&>-v-g7>ibF%PQ!*E9iTk`Q7$@al`Mtb=g3C2*B zam?e*BQ?J&y-E!pbPhnD*hZqrkuso2)|fJ!G?Kt}Mcu^ME24BQ>U%GAcuJv2nAv5y zwTWUfVN|AX`;TOaiOK8i;^hUp@-ICtCdpt0>AP49iqr>aYxfR0+jw~SZz@lE6BPs| zoQRYe({>Ob9igtVtsKn+l2+eBpD53_1%*rgSFvHkkXWwIsz8R+eIoq zcZ|LtDRs~MK1KZVV+0%#nLSz}=?1Hs{VP6$a1&=MC?~?_UX@|YM5y@!o5CXJSI+0^ zoT&#qk&c;lW);_VgvT1uD+2v=9-Uf`@K^@qNqV@Qg{ko*~?1Ex0#(!30qW&5e6JQ=@uF1HRe1%+zcGZL3`EH^&>cd}8G^whg7N67N3l_{tu-920FVX8CVZS%K!vr4}o{uUY*qZ(p$|J~F!`yxvY~w*L zOPWklDrfs<=MkpeBoPd6Vp9`>EZN}=@zcxz%eysGgS6v%b+Qq;K(%RJLwk)FI#Qpn z@&KtKOF)Us4P+E8h1bZVU3@Cy%13p>8QPZgfD`V&mu8<_NrBZzwEE`SMS%rnc?w^5 z>6ALkTY{F8;@uBRNI7FHm5dqpI!UvwU*h*>Q+d(6Y@T7dz8Q3T)v7)&${a6b!UQ99 zm4my=%F2QRFPLhaf*F-H8+!HVH#u0}0>y{3Fm8qE4^cap(7%mc7`BbdpK;6bhxJVk zccd){Yxf=Qp#!!z=DQXj()25y9oOzZdu*FJdwi^gkw~G#xc*wctP6zRi4BCeJmR=j ziqaofhc!@O!FH9+@H72Xg_f{zuiy!z=kPjl&bb$Q;DkV&VvsP{#I6g<`7Na(5s zL8uEKxZV1AVM0xOYah?zE3rk)4DN2YOxc{8MFUfO67_(#NWHhKQ@Fgax@P5_-3Zt+C!P z>mRs)*RF>3+&!yeF2yZBzDZ1A;O7X*w>2xT-II{}T|%@6lb9XlPylWrz<6sOZ8o8R&!(-omU}yoT ze4mPSK-(T5iJNl!acfsd`wg`22))wS>a}{`sw^ILt=2yq=#$WtT{QxP#C%Zku_kDC zaKn#*QwA?Cjb_tcqsMV&R@`!_U%qp5GK80#QLvb)c!S=G8Jg68CCadZt|Gy<7v*qLihPs|4wtW;{9DV?2WInMTTTC+s9c&hl( zQ4_%7-)K&3XKGicwXX=JY`Iy z%av99^L^a*YF`MuJY&rp=Zlm5Y>9FURSU*etf4&bFMpSOVBXs^(7K!@KQ%fIx>xSn zgzV{=&Sv;7UjiawZL&Ym*oF880_tK&Fv|ShaRL$i?!*+nMo@!*^-h`Z@kVObV;6>~ z3|>fj7bV@nH&w*X=iClY=Jex?a}DqDh?R z7%|k!Tu<_2z{{JSX{YTTOD%nSf_J`FLGdhnS35Jri-AWMk#t<{NG^-iG9R`>1V6Ro zay|W6-vuvn*DjLKbkW!E9YAnanbi%u=Xpg`{OQXuQ>?wZ2Y1DFv)I@^D(%LjCiqcI zX;CbyEy2sJ>8&3Lp_?LWlr*=`JT>0>I1WO`%`Hn58Q`%Q5c;g!50?PZX{@GlN52m$ z6zd%``%>TxGpmpN6t@`tYn&e;kvi9x3MYmrHImUA(U!Sh35q7?-sGDoG@rc@-`x5_ z2l0v5n(MpH;-n_^(k1WB{v@VW{NO^B3abtJ=`(=Pp>S2m1nZl`z!kH3o8%yX{=cH7 z2I#-Gc%D?(r{?^8Wv_@zHWiawV{8xiZr!UbRTYbDNKE1sFE~b9vTbpR z9XjbA+JHJWglfuBggn(7qwtOYo)I2)aD}SP?gpLlIiloQ4|=c^cGxiWfJxbDPYie`efj0ByhT!9^x z1KEq44n$5tas570*{RPCUs)#&kCjRL=Vxrdx`lKW2c}4O%Te>FlR_#kL#^;H2<2<& zAPfyvmZa>26-MUAjRflr?p>@6kXaK^YYV%Z>qBz(nRaaJo2?|b!OY4B{F=CpiuLV$p=s_flXZSDBF)hQsUbUxU|UJ8ZAVyu(f`!b2q2Xa+H@D90O)l}S0l6I0nUYw=5^huqTJra?q~%0ia3mDM8r!z@!Hk%n z<>Z3Iw~p#7CdFgx;s#=gk%P7XAGX|E9e-+6>Waf;9vWq_nXRC$J2vZk0*<9otO~N= z9`E_4jl7nRYCb+ChIklLH|SG~az7WYlrfz*mkjmTr-f81a?xcLhM=rr!5IJEZRqsN zYsm=#x!CxIL{p&N-^liLcLccTOMMeKfg^)WzlPTxm|P-d$6K+#{r8f%Hhlyg4eO!z z|8uIrAo|a8jpz@%^v!jLbuh1SM+p`FGQLZ^e?8zx@2U99*}8(@r$74%BiWSGC{l0Y z%&o-o_frsL(EXKRxC;NWt7PyICz=~`^Z2>{whv3fJe}kBDBpLkgRTMm=|?Ijf-YXT z1PVjM>$HD9&-2;k1D2&UEZM=H3y38|b=$6>j6>EehU8w99&Dy5q)^mN%)ItA=9SpzSSK*DbC# z@qzyfje`Kh5$-r}P>s4{RZ_*jytWZ~+G}+?GnIC}&W=6I)oXr4m*E%`l&D9bRSv-`Sgj0qZQI^W7}9JwoH^J z*^f!`c~F{>Gxb$H0}B|gOXZfD_fVNx?5A*x+|AOiV`lX!KgVhM(lK}4kE9)%@E3*0 zHUl#{-)U66C@E*Fo{vvk8F18MTei@2!#Y^W>V1DI{5@MDTm9OeM6+6@A7S}9D_Fa9 z^8cagtD~ygy0=wOLb{Re6p-%j21)7e?%dK1(%sVC-QC^Y-AKoG)O+vy`~G2^F*tjj zy<*P!%xA{J_8vuCbav?J39ePxDV&`lsmS{VID{dNI|#F|eWK^Y%tR{@w>B4vLkbz# zlJsv$$`z4R4>jCZyL@+3_nsMQ*+DeJxIE;~73JhZV>JO`bwn6Jzo@+CCLCYj(E0<2bqO+&CL|KOU zPtufAe}{;RO8p?ne*meeVRz0mrp=~{(=##Icl=E5M>Lu1k7ow-7O?IDx;8_fxe#p= z>vS}l99v9J8k&Q{$kFwb@lXrHJFx~Ow5Rm?yozV_XC4HKTsF$>=(o4W6*p> zm{}wJh>t0TI(UCg@UyL^BKbow5L->^7bl=ZoJ>vi3_vDaT{x+*621*ybGuZ?>m=So zh`cT6H~WUBPug)Pr;|X5+RuexyAvIi6g-8+od3wM%I0ODS7ze>Lda{Wo3vTBE#JAC zBk;voDXy{YCK(Wy-1l%96C1QsCZcvJq%;z8{F3{t>PU(y?e#w$v`oaC?sm&sBUZW? z#fi-=udl3ZEQy}y|GeB_cXCMy@ez&M7-dfbk=-{~#|rLYyw_i|Gw}Y-e8y#&jR|as z3@%;0kBCQ#kk;?!p@C_tVX7X3n1bIeu)imLKy`4~ln2)g0`9rZk;w%xW6Q9a*cDWjc-~ zwdy{d>n*Cx<#NTF?0nFhn8MTE4K&#`3}R}9-gAWSJ?o6hOn6}EzLSY~;t_;@z!}b24n8cqnh_qFnptO((MH0-4bSO69xD(qhaIB?xy4Q{2oi^imTi(wNJ*wwkC=d$ieT{mXa4ol=ZkcS@1jxb3 zk3YKJA7a)d2gXb|75q|Dp0`(y6FdFU_u0P%K>Moaa;U9VW%g`gXZxSsZC8U*&n2s? zW1btjhC2*Sz!%zM$Eazj3Hy|RafdTeUiTi6#roY#Qk7EJV!6Q?YMBYJ-NmB7<8s8> zXkn3OvzwBw=0~RvRBhg;`If9HX0^6{V5+eX+maEPN7n4nxEP8|M} z?>0rY8NrE*+8(M$M5&$DXKK+wO|faHNYojaFgpi=AFvpm*=s4CNDGrp0l z22pS@w7UFR4ac7S2R)!5+=xbW>Y>B? zHzoAusl zPcu;UFSgtu_?U}ywo4t$-i z8}=uK?||_W+f{h79ehXaVTe-NVAlxKN@Wm}A(>d?kM#hr1aAnNLbP-CO z-tfR1^l8oGsVae1sA@7mZ+|#;zSfS+QQxxDn_>Vm*b}w34*Vvn#3A7}iAaJ%uTka; zx>LOcAPxlhsSEBJO7`@N;CAG5xUCbF`W<+qME|4xGEN}J_gYU>nuTak0 zh#<+MW!D0TT*+7+5i0bLv^h2^bR%e!Xz(8W#}*R8*qr8F$ByU-JwC>{^L(nJH`J-8 zcTs{8*d^j;yK-o$SWkAcE+6yREK4s1?$^fFf*ly#bRAXj^v!t!FHOK|Q05&)USkZL%=xDtmh>I<=Gv-M=LiC!w{n1PSp8xCf0BSoq`x06 zCnLCZvfnbrqUIMDM#U2ag@n_*(b?EIZ|gLoTCE#xaoclu(cvCVYa{1AiMNL85N*z6 z$r?W5XGf6Jln)Gvt#p-=Wz_v-$QVb&^Gf)X-Gom%4NEj5>;0sA5q}BKAD@ulYiE|@ znbRsxBD4&6Wugp_-tW@UBK~B*zrljl9&7ecQOuQLZYv*LfIylw%2FJt6h*OJO ze|f(kN94CJ!UW32QG@;bzDZ(zJQEN&H8kMI9Ez(kfZ<+@4Skk%&K3sH10!^gaqiMR-*cb+Q1Al_1 z|Gcu^?pR0+hkQzQZ{!@NQ*>hpwic6`l+qSg)j+`I8e)js=|6-b+K&7?JHl6!?g;;s zH2^L>nh5CbbzttReJo)KTR!5@fH$zf!@ECTb-;FaGHv4FTHyV&KA`-Ab4t+cxbmAW z;muB2`7T&8-QwTCPZm2UUgSo)Xk17s*md%*zJv4#29sWzYp8wE&Y>zZ-?5KePJ}IDqoc zaCt%C9(Non|8vh=0ID800@vfOxL2fM29psr2v-^qUg+rPh>Ng24(qrQIUR~@SeJuo zxpxKsh4KExqjBqEAsyFy70c_Z(Z3t+@KQ|PL&(Ud}a-h-~C{mQb~ z?=jPmeWyFNH&50JXn<(|W@cs=%XVX#xw*nClzpohoWQoGOS=K@M9&qovjAe&mJcl! z{_=l$7w2z&FIFbF%8K-+zMfO*syG}T}SmZUXPu6wvGNz{>R%fg7!=i`2CM<=it|EOdZnE zjg!cCoMd=j6$ATg43jh^8(u6}H{xobH(V9xJoG2Nj_&FbpRRCM81ye?gNFI=@BH7& zfHE-LWcJ@fuusn>ikvRJ>V~9&Zj_ti>+xgN<3_=AZ;hXY@%fYL_qYC1=C{Q~cJ1(j zYi(T=h;3iLX(P@Bott=cY=O9fwq2MOO!w=Z^=$)`e+^-~BB+G(r7+V;qe4rcW!Ysf zwVsVHuE%_@1GLp+Y3!yaVK=~QBSy*d^U( z@4&nvziT|RSSgNeA)PPx>&cD`bu1X$wdvZ9hshs5V2lLBNscUhA$;9N+>U1fWCG<5 zYw|M{$i(v-6AG24PUsm$$XKq#QysDbTnOb_j^}C|Q!NW;;})tNsVrDLlICJ93QUt{ za_GN!d^z0Vm_i&2VfOh`Ia*5jn|48MtGQeZwqteN#*j{7)wyMJJjRgT(W*8y4{%H4 zWIr8fEXeb|fXuyvARy1j0~}-Ckj+WOJmW7vA9{T-3Pu#EE$NEM95}zYh?JrC4;Swm z#BqVWe7n-*M!PU zB$uQ%QMx$L`&)N@=l@PSfBoYsqqGv^{oW|sL~o$krlnv;w1QQHl5ii_OyG_F%CzU) z@BEa7AxS~W1i&23duHFijC)9f3vu8pOTGz3v{43@w2n*fhZMXg@LX64*{zXR`;oyOAntDZ8LOUsCv!eq4md>tdhpC4lbAB+?P1lLMHTDqJhGPim9 z7l-gBF>s?SW!-A?j~O{kw~if}b-Cg8HsVb(eDV1h!+VmaY&Ug1Njm5u&8T; zt;-=HGR5D&+@jEcx?t?j@q9<~_<2y)GC1!9c(UtyuE9I(NoTn);K?;Kzlnb+6S96D zMtku*LSVd^gG74wOT^cmgXMH2g7=X2TiZw2G32$Q$9xu72Op#yD~I#28QZ(+-Bd#X z5@*TZsK!DtLkQ?v6y0;wkg~Ri$>MMlj;Gm`K?hM+7OHw(U68JoT1{j}xej_LmY;wh z;KZ=PP8G>v@!0oq)5iV|gJWonWRKUF+A zq#CGWV}Mwu8@>E*RNoHUw6Yw(e$^rhXKgf|$pB@5^mW{lVR%kNp2MDmwsV|{47W|z za2i2(TV%)b3xd1S-u$&VQ zq2BfP618yWx}uy%Qp#O-M%Mu@(YTJE5?~D^5VHiTi64K{NRl#dKk3_C$uby%A)z{b zbh*TLn8i$q*#G7${EM$0foXU=Clq%{w(xM!e|H3&&n)xoC`E!@5od&lF`GVtOk|k#t z(m`^Rh0%7Jws2UAJ}sr4_R^R%1%1Edb2Uljk~OLoP%@!p)6%CCaX-&!QO9JTLK@HF zm@{P1Q~lQc>O$|Z>*|BgaTGmH_dV6e2m0l;A+vg9n{p-U*&p`Ft=xjH^xGUk+qL}a zL3$>9k;_vv zH}9%{7{rz(=dPRBkvJ}KQscPd7#dJS3edUqj?~*9W@y0e;4_*;?MG2I=8GhhH(?Ys z_FD3aA5H+RLFxu%W@wHn3I0$m7T3y~D=@8MTN5?TFo63sy8uG#BaZLUva^!x3r=Cx z&nSXALf#S0k6V3@I>YS2oe@K_WG1Zz9vfFP1k*cdXg+H{WyP3Bg~pUb9zBefL%#3Z z(fzdQ^BbNzr|^X3OM-}@sCpKTXXH&ob0-#lAotlR{dcwZPo9a*KRUWNaOH33}GpO3nPH<8YlIH%GO4q#soJQ zZ-IoZ6QO)l`JC+;sxZzv5>sN+&y`^6&kA8FfmOR;HnH^EDEP$EgH1q3G0iL0v<0i5?vq6A`$<}l9d>8P z8q8!88Z-^9CcGb5omI~)YNq+gOz7{cr~}5&Tp;AYMy@ak5#>m)uq5}qJuhADU8ZI3 z7=9Zd-bbQ#v*dv5g0cgnTxr~FWhJF`-2*mvw{3h!k2If6#~1yTXpufHRhFxQqNxBv zN?_U?CL|IMb+KKU8)Zw2$BTqQJV=t0exIE=1Uuf|Y03)8)nvc0pWL2>&Y1Voltb-Rap5ip_@QjBz zFILcF9*QN)tj2H*R#z%CXTX5SgfDg3`sb_F0wb z*n?C?Xf8!4Fo_viV@Ol!SAnN1*7PG(N1h~H=~@QHjR)e(iq?d~k*ocSiW{^n@M}`! zv`8%42V@IGx-=-!{vRB6{V>&`=%yJJ?Q(+I@N z%oS#z$?iY~m{|$zgK!gPpMvrr&;q&g5c3JW(uVySY8P%+p@IwDy_;)r>@sy?@^3=u z;>SOcnrP1nF6+Hir^56OA4{P%+>65G_lM#(%@lxf3<}pPiMEaM<35l#63hJVrq*=BCm?+teS5wbeib%vV?440Dh}wzy>~`2s zd&{sqJGes2x|FeUj!>KSzInMa<<4KX@*`^0@ihXHL{M5Nn{%b);aOICi1G?cqimC$jF zh6d=l(5rD5`1UbJZefPR`w~~o)ZU@X5>v455%Gp2PCyfA!F}i^CNe*MhJ5`+$28-# z4e6Wn#2n$Te6(K_qs|TKR@{(;LO2Jq&$ zt&I1We{_}*Se~BxD_$%Yi~3WUx8+dt-+0hCl(>U7rWneN72vNqK zbH0#oE?aFGfH7tV!5mn_M*SkkP%5%Y?C(NR=!lXZ^y`2&lW~Pq%SWTf`BsA-EYqeI zM|=0oSy})L@lZ#qbT=E%+UWMffX=x@Og^A~p~Dh39l*(qxpQ!(<4y-Q^DAWiaRH(b zzSS~&`B+rxig>#{vqh?M`#^Y-6`U%|f_2~u>MQ$tQsSlkPpnQ-Vzo;(N86nisLSzz z=EfX@N$VQLS`ykn+&~5xQu(vP{`ZmFbJ7ra-WZs7Xw`Og*fKX}xB4qIug9=GK)Feo z&ZD0mMp~NR)5l;{Q@!OT1of9^0)4TEA0I+kcF$gTQ)SI+-82Hve-Nuqd7comUd-Fb zhjGi|v zGMqIrVGRN}Ep0z&Ja3L1+pQi&qNdR^;mMue&dO@(F_)umE#Ex9%jLABuM(}c0jun` z3HTFXZ6^U0I@EVhIR-ggX3vj9f@l;A`ePpibKC?oj@ZRwQW;zv&>{e()jo`UP?4MZ zqH7FwXN_XiIVOK3^xm~G3>zzrJzB&mCKOPKrlS`%b?r?hGJAq_*z#BSHB zO!tOO{r7S01oEs-A&n_Hy2ENb!qagq`R)Ej8mrPVob?jW0R;NTVU;rk=d+rXpSyBc zZVUR9RCV{`6&IS8=KBHpWgqdp(kOae=`{zRMvLn)<~E9nysMf+&6Ogx6ZZo%c>T^F zBw`d@2*wQPYMXgs%$mkvAd@im(g7I%pp`@4wa~p<>Nm5VmNmq*x4PJfQO>T0L zL;d>ITTnhKwT3i*(3rQ^+5PJs%Yeuh(qwqOE5v6cDP22_Kn<(NR3SnM>L5|n7-o7%}d4}TT$%iYh4a4z+lr*l|-I@ z(14W(^tT&pyqmZWq~`E~&K1APQCNwT9KYh5Di08krXf~7>CsSmS1>7(5y2Tn}o>z>wSF%!c8)r z*1lr^EV_Y1`ju123YQb4J2YIzQ{-pUjdk?XxlqHZd~(c|_@GL3!_+8@1`4s~0=68? zrQI)Pf;X`&W-f~}!3M$S3 zj2ryD120(H^FJjmoXNaL&)%+%OKFaFhXbvk}U_e&Lzj&G4$1y zm5@b*yH#3RroZ1zNg;m0J@{I696~JB+oz`4%L`HKQ6I%%$HwU9_9x@U597&l$#Gkg z0UL{%y3;-go3LotTvDduz73 zJipPs>Bk=9E0X1M@cTs|DzD|c>=EWc;R0qE<{q;iP5*{(3Qy>cZYlD{S@29VD5_v= za56esjuh{+ZwH{r6OvhiQQ5y3A%34Q1vbkamJbCcg)AdA!JO=>nl+gLtZ=+_dVc5i zTn*JpU^kb|z4<;yYAyV-_#_u;1tIF0urr$9oY=UD@AWKRce(5hDG%k)H|A3^MHZ%_ zb46ve$w7*XU%7T&pAB7e0F*i0rF94%kz1Mv?)?}B9H9X!St1W@N0>{XZ&w4pRyS#W zPDmvlY2^8CjT~`nwL|$Q07W55`iC|W`9m8Ot=gH6{-%wZL(MgMWR=zYBv51Nq4z=> z*|mIq13F)!aq=1+Ph3&ewl!z|(kTrhb16gLJD=<-$NS%_Bcap12C7^i;Ld3eNZM&S z)qdFCL+|1t|MaES?6B$Uv<@!VWjMBolrv#t-@=_Yz&d;s6!`(;YDX-&Z9g}}_Y^vD zIu>QXfj89Xg6%dWsQS~1KXwYNE0Rj(8;vyFjMm}-k+Qm6Ez+ciOG1p;0(4!>#~z2; zWl=^H)<7@l8)&0vx~pbB0ra6 zy&ql(2U7gH%;oFNbOxM3Y3L4iBXPjtv{IREXyQ!qE-Q6Vpc`i9A zd$U9|r9Hl4Os>S&XaHUrtn2QA$cJRCudRSpI|uPx$dwVUpR@ zb!Yd`$tMXy*NS-Kp)EId{@d3QM&%PQfg0iHLa`1?xd~AiKHfC2tF-%Bm*HW|GzLiL z_7;cT4`7R zbWTK|1V z8*-T&Gz9QiZp0J<+3zw{nBt-b76^J;``^KMl|f%vLq&AtIfm?4<)ATIUB)qBnBe=m z%@7IC92_0OH&>Eu{01@kEz^u*eqkZMgA{Diii2~!0mF1wk};@@0$J146rje zOhAfNn@Vd~`MEZOU@X!LoKv<13QGsuo2S&6h@qpGYk7Yb4O&5gafMb*8*R3hMNaV8 z3PQ}klhSVgy)EtIuu{TJnQ$OQ>8sZlKCx%T>T*e>puqwoWk64%_-DU*ozhF7hi3R7 zp~d?q3^N{54N%}a^*BbwU4c2@uV*cOb*rurXU#A}jYXhFnlE)t~`u4D^6&&qNAes#R?uXY> z*c&@PmnJt}`L>D|!WT^vY>6_s2_i_tYKH{5waFkcD548(Jh82ZK-)!|2IvXf-LL?K z*zXXP_3_`O8$Fzs=p~^~1$!#&M5VFk2QpSA?c0jnsjy6_7Oiui z@h9Ex?W|L!*=aQ46(y14WWSKO!UDAlw@45KLa6Xe8&&{SnDCnjK#PpbA%mz*mX*R$ z44dCkLPPuzAt4@?agq(4iE(4_%m7bZo7(#$dWW<{CEh%2$kFrypRstl&^SfkWX3wg zJgYI%hpx4jYJD`Lf7V4!=sVPHUGTd0P94ie3i*7%njOz1lF-GlDX-pR3EmLBiaDGR zn=OEP+)z81$dXB`zDYgKhxUo$V2YvvBQb&)Dx&9%*ON78bA6{x*O3@NC^+}6zFO7A z8dvxDv~$h4xAM{1;DXqT#^X~fQFpf$7#!-()rAMdYPYxkhGG=AMJ@Ww`fBwrHFbq2 z{Va-}7O`p3X}y;v`Psx6SL`^gtO03P{!7D8nBA;1f_~Um zXugkWIZ#~d#e}17$fqXs=#7VLt)SvazON=mx32#9LM}4U0Bek%D^jXvkvF|`zFa*HDpqWYz^KWQ?TQq*)&eKx z5zRBs9p$ATD6bt&3pH2gO7-=fhukM5MA&%PZyY--^K|tdqjQn>rqYX0*3_3?@wN)~ z_J|PdO2LR`j4YebB@t6Y^|P^$k4&7kc*o#u?k8}CUvT{O%qQuMjboDA=f#H!F?tgl zKRY^)$^FKQM)s1HcmfjpB!3a0eD4SaeoUU&f1(8X@Bq6qQq~@iTOLg{BoE%l`ACMf94l$cl9HHjvX;IGFh>DP3Ax-PrB%!w_T+;e`7VvThsKFx8FX(-rJ zVoA^Oc#o}BRh!k@gi)cnMk9_3z?xK>*y5rO89_rLTVKTRV!6R%UxF$L-S52Tp0(Gk zA!M0o$9k26`#=~I)FC^y5V^=ia7m)U*!?KRdGmxMYouqSZZvR=W4x6jUg<+A^lhV{ zG5Ib6x=e}%tbII;)SO{(!-C(xD}?uZg*@gZOdTOtrKZ2!eHtK^s!7j2d~!)CYYbiN z)tUcHwz2IfxKoT1&tO&BDn4+JAa%m#?a2ND1yr-tY?(-htqF`@XPR&|ZGA!NoJnX+ zs=_JgUcShb&7YB-B6|+|s%!GWTt*pL14hYOf2Rmh;2ph4mq%@yh2vC99VbczcXWVl zD3rqxfV`>$DIGM5TYU3a-*v4DQZpm`(+KQ zYeN}ogr#Z=QZ-u>;#8|I`YB?1(0uM1Q8m>|ZV?@t-GU*0vn~?~irsE}hwaB`Xj;<7 zWmdEBP2ph~ZOT#~+YwH1R6~9_PdJ>ZD!?WheO|FVofG0H+^*UBj1`D79_y&wCz^Y~mU6yDYK70EeyZXz(`p971g;KhDkNu8cSScO$b0MvyDHpl56beWu1 zgF2<6q?+_yZJtgC$?WQBJvpe~UkMkJ@YxTQa!O`pQU4sAld@g-0iioP>oIRcw?2jy zpIL7U<#?hd3Tk3GcbB&+L`sQb8qw_Sryy)P+H`k*((}}Lbs3RZvE>o{zX6g`F)yfg>cM3pV5of(sR?*oJsi^UjA%lPzs;9xA1e!inFSfSHT(B7QWXgp2a{Q0H07)(_3D1cAoP|YN9REh zM;tAJb_sOrTdH~w*rX|cr?L5j_5()CfK{~918bLB^}E8DUIjawN=HhuduLOoe6{Kq-yN=hO+ksq{jBhm%0k%NcLzZd;wwRx+F%Qs9zCrlIHkW!R&hCHGZ1GMTot z^JSn($>H(TLO&{6S>#tAUv$2Im$o-RdGfZAtSUZAue_CY#^Q;dToQ>nWdYuJjZe8r z!cWsW7MeaoInS2!*#Eu@UWWq0TRMtO;e`?dJ|#FtPwm-t(<*rq(J#yXJrkd{&F4Uf^$8E&VjtnB$!itx5KDw|yDY<@ zD%J*^Cm~Abp(+(!UvWqx`w8=HX?c&eUOTvJ_55=kZ^si&nP__dsId zZ;S9RX!3iYgzr$`>QCiAXKk2VqV80jq$lIVb&Rn=cG*~~E)SHn?&W*bhQ_&L@>5cR z>nPvEE+-b#wIrtS6~YAmLRsEf5R}zKTgeudEG~aqx%1OLI@B%7ydlL43*1z<4IRZd zI;l@_!Rj}LJTT>9_`&}$lHIu-zzRIZlHctF*emi0MJvqp&7Mx)9ML(1Yok?N@BmA% z6qU_YFX9R_wYNiFGT^!2uotQza{7wgR6NcLz|#YV0d`<;svy`i_*^d;q1Qv&lK(u3 zm3zfm{aSRYhB3^_&^fr&qcDVk!;`Yuofc*iSzjuCyuy3f3IW;yd!&@G*V*^BcwsyW zm+wj?H~Hg^0L2R9>uG?8kU1>5hZz;K1pv?F!)bL;^zA0}A8Q2;E5<)&f5B9U? zB=m_!bM41W)331C*mJfh^jB;w7IY$7TI8xE=nBUduyIDf3;DGQ)1^mZeA_*84Lqgs z8n&3Mj^lnQjZc3;tAgt6i!n?Xt8y*aE8xD5gRS1PkpEGqa~czcqGR!(2*HjOVju~N zRsD_>>D7YA0{X`M$$sM2sf1e7i>(;~cO2RWRW(T9g1y+sgp5o!!pHJp3o;6Fb9Dck zy(N|RbA0uJV}Z43OpMXqMvHMR@sGVES@1G-a0?b>%u=GUj`Ln=3@{V5Vs0EA2_P?$ z7Z>80BSbr?5k1vp!TNYeNL!zZeHP`C3Ucr>EkF8L4HGV9lA5Z7*LOA(Z~Sz39;1pb zt4c+gpvcNCyJgu6vtf;jxV$wZ_Ml+&y!(`RMEbv8Uwa7=9ZH*F-RMlJ3O_F~ZUQ^5 z(WttAMa74`PeKUQa-tIH-H!krUP<|1H|_&Hu6S^ri_wttn`9Pevd9=wDDM@IIkJ^g zwn0}5nAE)M8=XX^i(DTGu;PNVaIJSH((Jx0HoJv3by!Rxfw`BID|vB^BttN@5sXV_j(h<%P7oEv|GaPl z2gi_<#GWxpKA^xGCi-0BQWO}53OCcRGICht$!6BKGpweSFnExd+2AFuMY@q_-a(W_ z`7ep^_nO$ z3rvY#Z=u9SiPQV#Y(wgl_4eaG&(Wzvmc!O``7svd0m|UN z&7x0=OEi^XXc-5VSCtx(O9yw}K8B{Y_j4*2hRPAh-S!H}@qPWb8NrhGo2liHa)yy- z3rwI`9%PNRV+geKOL}2Xx3z7d+yC`JV0hkj>3wH=I?-kN_efgs@}j46?ytsM3NWa| zf?fvzJ={$QqC$kt=fZG%cg!CMjaG%nAhkb(IG2Cf3+-|Q?apUyf`LIyx_7pYEEGno zA7EIA`u|5twC4+bPuvMzEX84i-KyTQMa|k7PHqaYAWbl*1ivgLvY<;exBq00@3D+h z_eEF0;+FxwBj;v77c7t+wkU(Ah+mWb&4R%uZ=_RByJ*B~I4@$rYADK8Qf##VWKcF5IyYxDUu@fVnW z4qQtJzrc=ksFd1UeU!{+pS^%(v?dU{AexPARTWIL?xc#Ii7U}M%K$xb-BC!uAv$3F zGAEkYxcJ_t?x;6fYMw3(XKNZ3Bko_;M!@?G@@W9L((6YiW=#=~?CiNv`xI2D+q7sr zz>Ngk3x4~LXW*_}gmlh})*~K`FoHu~2(=rnVN`a>a!Ly(eub{fY!Ed{6Czs(2xwV*c~m9nMa6Wy<4`j~ird5%xmR z@IAh-LR-u-wh3J?k34qn#Mw>M00}VM#`F8^`Z78>-LX$Oc@8ZEy zJMYE@!6JBss0hKpXI>9g;Qo0xZAg28j3!u77Nj^jY)EilK|%t8jw56|bW$Z8-+a0g z>v9{l!*$TI! zIVrmt#AZe(G!$sRbFpMUU?=$|BW~LuiF=%uF~?`Eo)}_jH{2jMlhv=5NX{qUoURs}e)QJZ%qmSg1BFN8y{G>Wdfy4? zvOsfek!%`Lal+j@3gZco91F;t;HB8-A&DJVTDg|f%xV;YqV zYvjZ$1-!Evw$}kOxnC5ycdPjZ>+s`~Rf;K^X-)My9vSWCV4lIel6q)Yx?6Gzlpya+ zn)>qdK7EWruC9j;B6H~gugit$Pa$rFWhqpP$8)&*>R#nmhcyCn@R&0_W_(Hyn}O2w=biZVJeD0Zo9oJ-2(*-rUH_^z zuSmZ)RSofafP;Ep9y(SK1D8c_6U5t=^Lm{we@hu=i^b{BQm0V1v*AODEXr+#?;Mb1 z=qUFI8<*T!fQPr@1~9n|D;~_A6ACz_{PiMtlvdx8*E*2 z?m~=2%Z6;UG0}{aQm^2vpp4aj0_0Rbdo`}IR7npE3XMW>9rliNIgvRe!Z_%}uKe%l z|HN-+ysbPcfJ=GYhGrdF@vTDA3c@IhtKZSSVLGt-~K+;=Zw#OixFG)gL z`Q{*pU38#;w%c-}Hh~keon|RdrHv!p4$WdgbR5`GsTP2e9S2mt*B)iGcOg@`%Ng`u zRn>iQv!|Mmr8VS-`Qgfk73=48cMnW8Pip8JeuHSVCm5`T96yUkYraC+v3YOWH8gOy zaC%O5uQ57V4yTo$)Xg5}_;6^{J>wkjcT^4j$F2XT+LZaGdajHKBNF&*z5;Zmv;%3u zHA^RVbHKB*iU~5F)6S`g?vE-*_0i`0<+)nZCxWt+eui`HtWTsqP&OtEcgH&`KFwd$EWN2VvNeZe_51U5?X6YTk*S#kuPa8RtN=$e9BwmFBPd5T~1|Ubi>4T*KcO zAudX>8cDgRtNEh3vwoX0;IAjWg$NS66#wtp@0E}GM{x;6rNgg-7^E{^Z-LiaM5=ka zu$aFu#K;@iZ-39qx`n>@EqJ&h6A~Jhe|Q?z9h&m$oWMv@hQKmQJ6ZFluBfw$;6*xt zF|jQ3c_~(y)s4&o2YN;|foFAg=Z$gQ>fEHqO;v7UwX( z31K0COq*JmC2n8N9=VMc#f3=yHVSr&;L zb#HcX_zoLgee3*|%i|yl_k(Izb_u!+qsG-f(+bH&d%l}<xV1Y0l;7A=+QKXSLPG z(Simnh+%1op=h--ew;D{C0L#^Sdhj&0k0uspYL}djB{1AFLkk(I)jc$obAooHC;o% z*KTjLA9HuMUForm09ek3^BnmpN2_liRXA`OtXcxHMIzQyr55^EJ(c*kgW_;stjyP1 zan$NsyWCbb((;kgc`=RDwGtyPB3gVWv-`y6SA(sBi=dlc#=HrB$!MuIzq3no`Cx~K z6Cp5+o|{*t@~`V9%=pejr8|7zG0b>Zv5-TeKEBEk_$>am1p z<-Y{#@8~WXZiVocJrvuMv-UU~P{uI1hIs|^t8KWZsucd)f>3a96lrK8uZhgv3lJuM zIw%*=7_*MY!@}ZNR(#OF%fpE;Lih}F?LRE0!!RR%I!tyhlj3_272IzehFLa99A6#> z&yHohkKC5aW1(m97(-0Xe7jr$z^VI1UQBe^XO%!PDaLEhkP)zXuHW$Z57r?hksj-i z7t(FsPrvmZXJDj`{l|sHc;`vb?(2p~L%RIPWQp+wbdd20@lj8+dvn&!2)0+#N7pJB z6za_eSuUz9$~HMt^gREiU_cfQ269>T2CwT4c~K_-z7724BSeyRFp$8fT_t-p?B`S7 zjVKrOKYm61ec#c+BpWu@`P(FRzi7vRpO#AG|F|f0;Elg^A|bw{-I{@Zx!QjV^#?V3 zAqIdjdPF^q9t^|3DEc>L_FLLqg+bEBwbdyffcTaTNN(+qWofu%3hnY&?D^O7y!GdW zhoq~*Mno};o9f!v7W)VF{tGCHpx@>;;K{8pu_wk>p?xcD9*CtaLlmdc!zM*mZ*@#X zaY~rqm8T8$f9m~U#?Q9TCYm>)V}v*m zq8;8l=xJqNWuX!5`t0>r4I_Z75wXL^cHHsgX>+*1!m&3?Bs#D#E=VBMYEwQ<=&Q)I zz<1?I$$l_>FU&+BE0qLAbel0TGldoTVL^<`0L#VXcZ{#h7dCU8QVq5kRd`<*PgE|QDaYU$jMuKrA@@Zhy9hts#M);m!UnT=-*^Wvi&3XO2J zP9cab^G+L+Hy3!?-#l`#vMmY{F|IDyjfI1kTx*BbC++mZP72EYA9@e^{RZMK_w6P`eG2xHQ6dM2O|7k67Pw z(K|I&{dYIXJ_t>5sf?T!VekCB541>v74(H~urAX`iAd;^lm%zX-qEtvE)DfLy`)7z zf;xO`pCFjO(w_PI+gZ$ph4mo>^H+K)j^1q9?nsroi$N&zMrBYqeLMM18HIitlSsk+aelJWJ%|d~ zh)}meqg481x@FMYmm>gcxs?NuX|8 z+x9KMI>o+wf!`SyN}&6^X+84E49;;Y5oLynM2;ajN&oJosI{K&h%4Ym4qqFE%D{Dp?oXV0K2WqoD4mE53)y@JHK~e6QcA{ z0>PD0EJaKeT%jOCDiF6MFJySZ9qKdf&L&ll-cz3CsiFOBl6nM}@{~ih#W&zA_fh-{ z9!nhhwItPU<;wv=eH-ac%D@U^-%XX9TptMILln1|``hz(T0Lz+u3(7dq<*ic>>7!e zc}hMUZ+$k%iM|7K!`)1L9Q_b)h3pV-2&6!by-_DUHDPpce&J9{> z0e8`#3;j-)I)_8H&7JELsFq%sNF!N-wjub;a|R+ZY(TO4(t|F*IYs80pG{u(Xn`lo z=G2`<>sRvX9x9T2(#z&7QMdA`W5_48#*6um_`_F6>m@lL)( zc57O4h`+k&)GW`X$6-Oy`&2i_tTehJqGHy@w8i6(ui>9M?e1-zuo{3bE_$a!`}r5R z5rcGvE%IG5NijARoK$ah{vTau8CGT2t!+V&?(S}+LApaq1SF&prMvs4ySr1mrMskS z(cRr8T^sT7dH4J64I%yV30&TJ#_b)opKXLjxx>EE^!hby4~Pvk;B3QOKl31w0Yc+3kNN(v&Nvka zlOxv?%3mC0^i}a~@=*vv8}MR+!{{@?G$ryI17mM``t~iuc0ID22rl~sP6{QQ*Xi#F z_Liy1qRGr(TjS&B*f9-h`sV7jif;^3r%iOKP_wCHM(Vgg$2AGFX;cy0U%95Myg5kM z_HvsCW3y5{S%p<216q$8JS(r=RJFAsVu$_0WCuWTDm?;ly@_CA+bZQ|WXX>q=$zWN z-|EA*J`d>7ZXc~@s71q59V-67bg7-CdX&WiIo2|D$8fP>5<&Z(R~jeOrh0s9h9L<| zJIx%L4NV||ggy)#`EK7qX1ZQuu8>(J7d9Y zz~c!Cn{VkFmFZq*qoKMW&0b#mM&(@CqcHM;(Q6yWSvi!SSLrY%X3G)=$$1Z{>eJ_5 z&0mIKMKK1qRRkKVXC)p9w&kFsd)NiUOxxGfDA6MdqJ~`Vsli3a>4cWn7osD(fS1@* z+o6z7OmJu|)P(1hn!}?as$N}&pV~np9Z=>$y^U0*I=dZR`B10v3@NqU5je(`u!DW^ zr4|ve!_$Sg3s}q9V0dIxv=`6`aSL^6GYz|WMc+fZ9MQmhma?da3!_Cn=)sCo=0_*N zxI>Pc)4bmPK}*FKw&QIPO$7Nc+D-S(NysVW!Hs5m#a*fL1?R0*FW+ya9DHnJIf@x z3y{8s&%eFS4ikCJuM(R3fS>E=fh zWQCjeGfS^mrtQ@*rZw2IrOS+F3HQDwEF7U+$xFF{7N|$1MAn%7s$dh# zP;ic%LcdAZR{5npn*FS!<;yx6NUG1U>HYKFr~A2IWV)QIiu?Uzm095z{9zT$K8K!Q zwLn!@Ap%B_^usS3!dJgnE}DH=A_v!JxNjR31LUN|$e-rFcYe26KpL-*8`U=x<9R@Q z_!wA5g|#T_oiQ7SD!JY?^vgt4 zBx%RQB1%VKrmPk*K`vtG9VAAr(^wav0^Hl+*h)gXZ9`O?+)y`Yp}bh^l5!!@l=nCZ=73I@zbNblcYY%w8l@b&~;Os zY|;eploxG}_loAkyPxGLmj)~~^qf!>e|-uMVSt=+e{X>4I7XoDN*M-a@mN9x5T4>v z(F$CvCO)4)Dp!zVhHfEy||8+vnOg#2bGlw$pW1)07=Y?~TQf|EW^}H8}lUyQ9#I#(= z8aU<*=hc~>aucwmfa-6MEijiG?cL=D0*Trh7aUg+pE6cl34+=+ZsBa>KnSUqxP!9^ zcl`ppyJlH9VZvDcE5!!8F-x7Gl2sYQ;dWO^rcsy#%Xiw*dKV05uQuKa2GhgeXPsht zG{Z*mlPPA$VRUFg4%Jegd=08=ye`s%GSi~0;dK*r`^MTi3WO?!zt;AJ;xD7UYZ~nm zPL)F6q|!ACQs#}*f7ym=`-uMVwG(>}8bh99! zgN?y<`2iWFxv!^3Qgaa8B?DmWK@Un3HvL=_vxrxx^SmIn%_zBHHN*NdN|ly-JjGQI zR*4654s>?`oGznUXj`DJ<;16sx`S5-c<!#H}6e4NY?&5J+#tbE>> z%&GDFAjl+e=S=)j=Dc&4 z>-Ls)(@T91AJ8l<=f1)=q{;;+)3|LcpUAOw7khj0mP7E{6S2YQmMgjW44b74-J%;( zLF;q%Z`?r7pGj1e^7&6y$aSh#K_pR|%N;EDc}Kvk9zsG}C+3`|Tt9AdHen6LLHv-& za{&4Z#kBkjxi24K-`?UtmrrCwTWJpf z_>eFV-k#t9J{Nt5%=H$3D%)k$y@4P|5RU#HzZlH#oqbe&EM$;mI0>(G0{yQMJ8*C; zzcCV3JHvrN-_3f_1QnvQUk1BWOn7NIvNZq=2t?i{n&x7zpKR1BM7L_&pnm#1v^PU? zpdWGeyk<_Qb5K;m7^c4UwyhtJf7ZFnb~Gr-!1x7e>m9+;#&qzHOMYtN3=OYj{Nu03 z{MMvWk6vV(KJY18hOa8RE`1d}+&s^>+f?$`8+(ZEja=D5u$~eg_Wg3uMFjX%F3pmt#s(LvW9jWf4YboYhw9pr+YNUGmgOu>5Lp`m^(#VpyY( z^2}MYuZvjB%2ZS`FNBZ16Gj8u@a89FE*0qdWR3H;JJ!H^jA{;uy4}*N}rZHThl;LyamtKHKg4wLqt9q=rV+fZVt- zi*J8|=gIOT{IKT8_(qSf4thuUMyBq8_|nh7@xds{ zP(~UT>A*DT(52G^XP$GcR@-y#P(SIi@t}!pu7;1wbTkh%7SW|@z62QBbjUI+6|fJ4 zCB8Tr45tVQgB#TV<~HSc2!`x)846JaVG6;Pd`_?HA=AF-=~<9moqYXEH_AN2u~{?= zD758MQc7X{3z5=SFW+!C1u9o@M&lfbeyUY}-%9tcT}_OV>sHio&BE^X;Punjb6pE_ zk|i#lIKlk2Z0GG%L$iywO;f4`d1zX8X)wOaLkEn&>sb(wD33EF&eK(3ULgL(6kdKf z^xgZj6;>Zpt7P1R>QKv%0!0PsE=aZw??y@9W4{ZlZ0!gwXw0Oa```jWZmw7l?z*z9 zo563)3gZGNoSku`M}IjSy&YJRi@xhH0y!0*xfVN{21VcfV&?py!6+Ak+xo@ohz{Hd zVSwkjYHea{&+~5~?hyE~9_CW;w?4iN;*O#Dr}^(_bEdF|(UI8K#4Y!HKWw`A zm!jbjz(0r($Phif#vD#YF#GtiJ6vSt-4o>84oNlESv|Agobf_NXKU1Ymm3WK_E(=5 zWTmqL%{27TR-woBPl#!SgS4$46M?jhUxv)2-9+9ZLdiXzR+;VnfwXC$pgbe zT<_gju<3uW{5gbsY$NcVIT9)1&#RZNh<4ev$V!!9g6Xpkn%Q0d`VM*ZG=RP%5hc26 zFk<;^Gt=s5RhBmTHUsxKq9eX6!&Y`pHvOl>b+zIPZ{`Fh6ZGdpah9;gDdeSYu zy*t%X?H``T*|&{qM)c(QLA-e^wB;>3njXIv)X*!)9&<=UJMd$MDB5}f`(8^2_V&=( z&TulzB+H$cY-7fxtpoBX8tD5`9oS2a68!{N`?1{yjRl@6J^YTnW$_cig?=P5ecLFU zXX2)zV#Jmj1<06TGru0dv#|fY1bg8;<>^`a;dWZ;E7tZ5ymW}T7zn~$*YOe_-*bQA z{1%f9ldhQ=%_CtIQKT~>#PHHU+l0b3NL{tqpD@nKqr4ImE+hUg z$(JNGE2kt_N16#jWHpIZW3n_UI&%I!%J)PN2%huUzG-xPh?&#*p@+|>{ffztIo3uh zpxX}j*4vbUF5lIkA$N_IkW`6l`Qyji7VP2V4N60yOZ)r;5jJ)4bK68R`qnI|J?!&mQ{eo(DZ09)tV z?$|(9lnO^&aPs}gBnttOiD`ZN`NuoOP!`HJQktA><1PLPj6gqJZ`kCW5W}Q|%tvvb zJI{JEK~5*{%v^Q6IiMyEOOp!?h(4KUpPG|4ffpAfua?JxJRCeGxh-=Nb}5@icwe_I zb&14E!}!>3WXkuR_HN=XBX^S6VB64r{4N+F`**Ivo1o_gK?N-kZ&lX3JMC!XphsO8 z$)bbs5%-$5i8@(6l0T^<3{NWhd$Mvuk`nqjN^a$)2~j3*mjE;%eKk0zsA`G{E<+DL zO>d}A9{p@`#t_uVswpv_dtu&ChJJ3P=Oi>z^~HQY9q|-A&3bsl-2U-@(>WbmDYl{j z06!D29_9)Ln~vuv?t9{MUqF#yuC|qaV%yjN_fV3Pw0Nc-E<2!kkJ@~A=r5rI{)6bE zPt^W{=(7HU=>8Bo$)c06eed^}twG8yNL$F^=Yd+(5>keIcM@#FsmEJI&pkrk;eZsh z@@gjR5!}!0aOF}pWVb!7m2nVX2@ri= z;RW*Da~ThVIMu`PDz~!Dl{r?=S#Wp8wJ3h&j3nH62&fcg``zl_SijR+xNc#-`er-f z7;g3r8%4$V8YYm1AZB!c zaAbQivxK+}}6+O%p8Khx~O@JH>G4DjxwJseGqPiAJas7FFviE#QIY3=ONGaoSpb_v zoGvt%Ygk|X0a8?Pr1mJ40KZ^N7SAL`!~U)>soRK|MkLdIoIAiBe#1lWFaKG-Kud+Y zL5i|0n_+W^#_m_KhHsbQ(5e+Lj-<-Cy9#}SS8?e#(T5zNajoiR7_is$3=C0`c=I}ymy;Uzh%Xu!2{@S^gS&c%(QwCq)tg!>Xflln@UEG+QBiIOIZxK zk@oIdY7d>;&x!~f>i1iGC!9iTEXp1iSHWA zH6#m$SZ;Dky)Vy0DEkfaK)a|L{{`}VISMcXl0U^~@$DHUm7}+xoS}gl9dpiP;2;-X zFD(W{nB%FrwbMhc-Vy#B^35B4OwU7Z(oO0$1IXYRt)r`Hpi@Ue;YRiXJA6Y|xz&{K z{p-}uJx;p6_lagx&p;8(`<^6raI!8-7rmzAh1$6Z!g;5@iv;vI>8@p{o-4Yyde6Md z@EA-pUDA_B!6tR8ba@)8JFkHLGg_~~j%!1RaBy~sNs*BQV@~BcUFuQNRa|{W76GxM zOeHnlO9QG5p5xxY-p_DvUyPb|c9;x(NS82T&e5BY9?9`y%FZfE-SLw1;N)KsKAa-# zG8E`T3I5OpIwRjG+VhLdGP8pq?qY<;`u$12ueD|#cP*)N?OKTvxu(!{ZNK2Xi{E$; zl5ih%c4D0{^;W1?ZxCI%jCG_tLAX?Ke5=Y3a!?v}A(cw?eE*?*8$w;3 z8vNa`hYjhrwHWGe#K((~T}5&+pn`(Rc#ogY$|5<#3*my7h>rqUD@@wt)%$fL7}i$V zP8cOC7bdv;IqzX}3Q1$EI?whFcp!|+voTs*mGllzFiN{eNu&nS{Me6WZFvPk)5$r9 z3B3Z8EoACBt+6BqQ)5f5(4%V6831;e*@F2{#G38 z$xa5zq$6H?638B3^PpwI*E%!hnvGh34+i3>i7YB2@IV*3E{C{U}WZkbUeK8Gf;O21$E6Yr%pWxS_Qd zSRDa9m)&L8bz8bD{$~`wyc3H8_)n*0yApyf3_r&r(UpW?*CBoT^p_Wg+tuxTQF%G& zuONXRV5}pliO3Wo2L9w*C!?sYT2&yZrD2Bf%&yDZDE5t`v#{QjW^ud=kIG5R0891X z35tc@`LQjUi3-&me3UB$YP(bhSS+^haYrpnQI^Tu-OHa0xpnwS60Gv;5W*Q+6z*2j z*Hz+u%2i-9fC9wsY``m1=iLGMNp;+U$CzrHNtV)ovBAB}QOxj+g-*rgXmHJrriiZ7 z8^|S0P38x!B@I#L4@`RUQCmEhH*73)nSRn+UNn=0L_Htnu+5-tHAe}} z4)I10jesUcm?{=Ty9rgF@DDaFEx_N78z>Xt)FiFKiOD{D1#h6dJs5qodQ^OV$?N7_SJTJBw{$16_hFY0fM3KN_VMEg(?U&yl&MQD>#K0aa(Z=!nIVSx z#(mTg?nDbxGCguuW&pNbU+cUChBuM)h~aT>MkqNx@U0w?y!Py%rR}##sfS#t_Jn~V zvFXfxkZsk6{4s;U01Yv;EN@n7;fI*dKjjX*;>YzBGs(*=@;E;=p?2Zrb0j2MP zX(+Kd9xRrA=21C2B%xzDWLcoN3@Ocsru{<}1)s@cllHxrMX&{hlcExl8&oofS=Xqs zih5IeCC|%jY$IWsDSYd<_X?$)LsO{B4zIhrM^M6AL~n>Tb?@C%#s%t3>IO5_{iH$jAc;)Zlb`)GP1Q6a zSrwFQtJP#{C%-1#{2kRwKDt`o{7qFI*qFp4!jq^TFv$JmH304zz6d2NjBTn!lo@S< zA(<>vjRsMP|Lx4x$LxkoZ8}Y4eb|}DNas_KO(hg^;rsdB8Usw+@;$^` z6G`UEO-Vzd6lf);!&KTu#*n^1tB0g`z>ela9eYZbP2#}E;l)ii-+?J`@=9$>Fucp5 z=L4Z5Nt+gemgT}Q9vrvYo>hg|gr!xnAsO@LBTc}8$gVvP7h!S3=0AJQz%p}t>Kj}) z)H<$a=r|1Dw9ZV0JW%d<#nBqL0#@Yi2k4|U8geW@7s%SEwQ&!fV*z9XVAn{F>j+g% zci;;12t%I1CY199C5}NPBmHKHSY!^-CM}SfM@W6*OBWFblWwa5Oqg>ioYt$30W#7% zomhizp&}<0)mq6S-rDp&q`A-~1HF^vMj*%{hcJ8btIz~~<2WZ-hh3Nisd{TdQDm4a zVdN{8k9eWKP{8XOfQ1WEGmW&Z912>hQ@L{`I0?2r)nZMNW1 zQQueE&3nDx3j2UL#Pc>Bw!7^}aZl%xRi zJ2Lk=M`}|)nA)pKHp)=4f{m`eMvHM2mn~P0lnmoXNpiGU9e%REZWEY&ese)Rz+pIy z0=e)yf60kTP2CNyY*%To6Q%9#Hi8l{p7S&gUWx|;#&DyOk+ElWT8&>J^j$ACwMF1t zZ6>mzR%Z&B-oyawv7Z@uA}v&$NqBug7ZVqH7)eXx;HE2Mmw=SN=WXK<`GF#Is8>U{ z)uDP{^-K&0rch4D8~L> zUKL3j$3$2SA%W-NY8B{NLjPNZ(^pM|qfG_{HXiomiLQp8xG+y z-_T~w-J>QKG~+@b895z)gjaXLDRo3`5cveoQ}+k&uV5# z=1(RF5Jf%s{98qb4gNs#oVIc+62M1f4Y6VRh4i=IIvs(8`68bBtCVy5o!cVnnDF^S z`nnI_f3d=6O9%!HJmX2|o$sT=S^olpP^ICntiXj=L4hBFcw|!W-am^6y+b?-P__#9 zU-REg4k6%Gnp3O#{5GZa&v^4${MDIRUyHZ*(XH8c!LdhA10T2zy6UyEUFoksuUirx zPN1Q#Ud0x0Vp+2VTe5XuU{*1!wN^2gtUfi>u#dZal*(;y6Lb6Z zl7;RM8S$AfH2k54z&5g}8TXF$mUkE$_D`Mc&MJ2(#S$D?tQsH*8Db$eB*R|;nW?RT zTPrEN=H4EnNm|eB)CcUG=RnWCE#*hZ3(L)LfSB={TdKhMNrhN7@hNldTs%{zA%tRe zN!@g(?IwIUBNB1jlN-W07R9U^Q-k-8*>o3)Y!&laUToSMf!<}5x#_%C_~Ouxfvt_r zK>hR|D*%cDn*=SUO>X(gCDlBh-!kp880Eftl`dVsA(Ve~2VWCqLoQS5eYp5Vl<`uN z*uho)4O(j9oDI_-2^}?`3TN8{V#m59w=4%<1yuT>uC<>3&Cnv#q6P#X%vGt*!lMng zvNqV%J-Y5|^cu}?cCAUev@?=aYxcm{2dYcJZ?ZLXW#?$*R$89YI&m&D2<|YvYGk{U zPyg<#-#-)Pwdk{A^EJfqwkhK4;im_B+@UCZ<8QS@b2TGHqV4>!ig`e)0DSY&!m?#W zUl1Ku>8c4*C>~gc&-YMw(aq-^B-w};Uv%!U(D6e;3%HU3r{=!cIsP$C2@vxbw=Q1* z?fQ=3HZPAv(6#b2rg`^J{kQVN2W&*tBe!ai35H>bK|M-pucXm4MlL3mZXt&oEa*Up zYVQvQ&ENF>^ImDGn(a3;K(>19BwEgnJV)}LSs_(NnA3$-`srS?-;taT_d2rkAr^F4 zRr_rpP1Qpka!o&-p_{G%IN&gH)wq~U9zJ_?QjY%O*H{ifU*D=b z3}7r)K+f=t{)D?E!G5~1g5E=crrWcyJ&{|Zlv#mviMYt`+Ct6`MX2jD`Xyl$@mDJ{ zMuHnbZrJ~a9zuS3riaU3$yI|bYyCeH15^)C4!8idq1trhJrmYOv!^@S&hrq6>m5sK zJ7(yWWE6NM`b7z!=vQ1jd!};L-uQ~wifNhP@A8heY-4V#N?mi0*j*h-o5qjP?ALi)$ z&5Z36(;TskMdsz)!IS(*bf)cro#;k6onqU%PwN_y4#pM_xmP!rR-2spng~A?$J1bs zztr=LD;X;?;MNiz-#qk;ihdW27MeZ-+Kg9F>vX&~uie@786`AvFD@1|*fvV@tRJc4 z#yu}(8MAe71`G!=O8t@uyAfi)(ky!7%|bHa$du#XV3>c0(F?^BF;f}?_)p9R&kid7 zL~n@|A^bkQBOB*HVpRu&4i7@#v*mW4QnMz+<%D%3w_UIu81;fvx-(MWgFO!Szp_{xb*!5gxot<>Ghsn!s9InA;_^f+LN6U35ZM@b^~#_>E0|CTPs zQa2fo;k?Xmz-+Z1Q93B?7_-$WCA}WbrOPdNJaWxSMOo+q?_2iY46j}3y1AiLl}zi> z@r5EjI9H7mz9L5NhreZI6!5HRRL?YJ1O$=W*NVA<@Eo?!d1m0(vA(g?1nh}dGm4#kNHZjz{;!H!yvdZ`YFY^8~E0)bRE zeMLJdm3Zx??%DWLXU$9~7KIKf&U*)H-oHs=Ou{KL+dXncrrtP9)g}yDvPTNeM_YHu zd5#W7GRR>pU+@&!NlWCw>`xh$3{;}1jaS*(mqb3(7;N#DW*HI8kcAYXRaDuCmmfxa z#`BozV%l0>LV+Hpzf)e|frjq6r8gh6zavbY3b@-TVYSgL2%{>V3Vz~yw7=w@oA34h za9=VT-Jemw_k;K^5LqH9CS}DOp%X7yH$Bd2_qnw5LN4U1gNytOb1yO<0?zi8Q24H0 z8d|O2iQkAs-SpXOiB#rNqjlYKXv^Gte(CrRM|5>+2QVx^l(I-f42V^qr8BrIh{CKY zQP%Qwj%mGHbM?t?JN-fE4MFLy`AEmYqK>~6c<8-8U7V;4BSIDWkXc%(NU=e4X^q%# zP?bZ8rb!I#spkL&=u6x{;$w{yroEo6PT?Y)Zu_JIOD{(DE;IA!7s^PEmIPNP-z1eU z2gHYfyo&U0^Og2ybM@G2jGZNwy5b-Rv8*f7f zrH86meu~r)Xo+B3$%HPL@h_h^>JW$*`hytpDj4NKQ3#E^PhG9>;>ZMTga8IK3)c{6WR_Aj|&85F=>K6HYg3vvyZ0IWi$_ zTtf;^nOqNU+XH?wx3->o3{>4KIrtSqI_CpqmDW7U9cnuDvnHLVvzDXbDjaQPx$9JTsZ{6@jmpAK!+D zbon;REM&;d{gO6cGOp9Or9*$H6B}1bzZ_ypyCeu?9LZ7P-?En$oAXZ=ig#PdeQs!i zcz>$Aogrs^SFv~fDk{Ix3Y)srLNrIDLo6^mG1gy6RCw$}SGq921qb{K>!kq~L*6aq z?m@eZYMQbYPG=Ovu~&3T#`21E#imXucJ)>h5r+=2R5Q^u!_~q&&P<~_QJa|NJT%9g z`Nz+pNm;{S>m{;#QK1FcVT@2vxEA zSV|sE6yH`XB{<_3-wbnY2f-j&PIi0-vXQ34)(gtAvN;f+Q7P*j3T$NZqogj?xjP8L zrm!g}NWpB0&q#Xp(1P>5S-|tsYaQC@8zuMB7RSyQ`94$%Xo+Iyb!N!_hK{5{JO?T? zQfR3wUv~kyPad3z4KeFIC6|4+@7Qa>Tx2|%+O4U6{9)(VkkId!Q?fI-$ns#c1RlTl zud^E|kEzO|38Ur`GmC;sKGTEeP?irc-t`(8x2E*}V~EP!dDNu@8)YF8e({Q`L@Ybb zxsW2j3Uv(~7NPH$<3fxh&_uGCH+XadjoM_m!$WJy zMU-8NctyS(HJ^Rxoy)hD18Ep>z^OYL2knksio( zyF}O-JQ>A4|Kk3VF88{YSp|3DK{POl3V8p_WXrd(vLDnDb~cptr|2c#SFu3>SXAkk zjb|t`Xy$&5x)f#tES%hu`417p^ci>Xl1b_Ed3F&@=apLGe?*#oXkbEK_@5M{gk_nDs&02#X?m5S*7p5r%Q3>;9L_tqM-bqQo!GHjubR z$6#f9D{kk~I~4LX-BDjL|Kz+lpo8=6${ta_@@uNN{^o1Tiwo&#w95M-0B)RX8 zHX91a+OK$Gs$SoSzJ|8|^%Yj#DKPIc`xwZclog1YJsv-a&xeR|GNgDgAG4(koXm{) z%Kli>S00~e-jNyb)6-@ECsQA-$c+G?sB4>`HJf~#(v7Eu+4+DNO8C6d`*ng1W@>_A zx@}NJeG)I$X7B|pWIe~u8@_a;d4mq|<+vN3j9VRwiPLbhCQAVVb0xprU&;^`6zY#J zazuHa4}vb%M5K969MVOV?$8KE$2W_@IJ2NYx>pQwH2b&Of?|5K@TyhiYsw zS)$xrYJ5X8)0pc3iCgN<*$s?`n(0U!Y9jq`R-aZ2Zu^md{kx#~MqOi4q$5AZq!U3OOjG~E z@o!9#!Jyp3Wx<2O0_gwnLzVcvML*#eCE`&-)*nPj`+^8B=M35=^EUR+yZyFB&M=zX z0iHpuK&UgOuWCc}tto%yhuL;!4nmqjmvP#9OWa4y)LopX9HAo$7d zzPvD?Y-ljw=niV*5szf+tw9`e^7rg~uNn5HSfZZqjMj;j+9bvTGNGp>eIcKM!$K*L zuvn1(8aVcoOnt8spvISH&J$wOGXItna%xG?|6OD?u7Ov3D(U_h_nmev%oHt4-zb$Z ztB<)pLF_`NWFv~B>AMce=p#1B2nXWn4>M_XM;_I2$_RB5x77Kznv{fm-*%~nMi-zP zN8gBjT%Ng9gf~Ms^sHVIbQ7JC5Eq(NWHpTkRCQ4W(>g$^ zZNhwZxc?JHx8z5AtQv+l7&F~kq>0f1^gccn$U*ddWF(=WfJCFk{WO6jfm_C*ouR?n zBAo^oEr+MubG4KGyy>#c+6@`w^Pgf_Ic^FRRJ7z#-B}7s#dvF#@nJiQj)q% zp0EA|S#%*+Bq`WVW$S=Y=b$PN)9(y)BMEBuLgTh#NIPxjgPkxWfRRM8sXiBy+@fF}T6c&)}Fw0ZIM4bPsU!fNx;-1A&`!p;0!`^}&II6=ISX z&-b<2Egw%!#lA9&LV(MYh#*iV*U2-$EWmdqty4YtL4G& z$(C_1b)**b%*WEPCW@r}1e%`+HIp=sP2qs<+2cS(;-$hY5}4#Ul#j^pi3M*ERpct3a8pZ>9;XahF*7bZ() zK$ba}iz4n&EIU;ylXoSaUrf3rs%sa@YOh>T7-Pw7_Ui^K66wVpRqt^U56WAfEvm$n zb=cn;UZbqMu3jxQndVci{>hKwO9!WezwXG;C@XznXXHf9U>%N95+ctrrCe09{^J8n z52o=Jyk-~s?=b*=SLJ7{U=thSh-MBhW|reO=8!~vaysP&LNe4&FWzE$EiV}xT8*h( zW~r_%<3#nIZ((ma%HE`5{A~p*iq61;%rDAnGt#lipIUEuihXQ*P7gbD#!y}CxU|#` znztEV8sPMBE&0zly@G54mpN)iy$4cQ!CuNV8>S6`%6*>Qzq>|I4UF{m1{# zNV(^dBa5#@k5@#wvAekd^Q>Xbx7?|<_nf|ZD|COV(x9|#{fL0i>j<%}?ws2QXD;zi zwLQLrpGNWxQ?wKz4NcFld_uD~5O_1G>Nh7polh)=Vu2|;B9hq!5nu9p+TwHO_m7#r;*SC8W z9krN+fInkPoc-!a9m%1DoVsp)G1N0-{g<76hFr1q)HkEPX6sg@#i=6?1rfK!olkIq zlJOMXZSRa<8#k!@1T8;LZ`z8%icou)842~>*5Q~yg$j+5lEy%WiguQ00Bl(CWf20; zi+AX`KRY#D8f;@?Vr3oCy`8xF`{U$P88G2`7q}U0Of#pw2x&6uyr(u+H&=O>O0`0Q zZrR9BwLOT57!>|2My00NGq_4v=+SB(Ut)O2-5+`zdOPPrj9kE!^2N8+m}-2`Ap!3KP@9qzehqv zU5Jg&ENot6B4md!{5h!tmhzV5)!SRsfZ!k1li9I83-*DLXrR z@6Zsz{(in@dTJ^`bp`8*CG^bb+!x<(iDFg1Y_eWGxBvHqBc6h|esU?Eq#p4HUx)op zEJuEs^6!+9^_;<1XXeAr;amOlo3Ha6g&{ zEv+WKA&iHobidp5CG#ow4ao0fio?ct#M1LH>8&d$hhH%D*ra zUGr4Ccw6<>ta+`Z3KM2~9IED@ zP{vfU07fcgYUs>2RnKTuUxyMqRcn1bScgwv9!7CBW-7&bs0`%zXn{-q15(M0FxSMd z{X(PO6snb`^0sLX5Lu-H6*xr>Dw+4!RE~K~Fn(RhBs#jyR(=J_P$%1C>W404m{+4Fg>AM<+C1E+W3 z`K>}spUa!-I0q%Rk6sKT@f>8rf`8*13_RS@i7c1iW5G8BUPShwobP0G(;@dcT<e`E*xwd?KPk6EBK`DYfoceVX)t!9CDuq1!xmW&iJXHUw6I6WIA*#F-6^~Oaj-jbW-U zl-vb@k4jVNQ~kE0l3Ozi`Z$g&B%^07d;6-CXTXPTzPp4Nqu?JpQp~!MInZ*)0)E;1 zlUZ(avh4|YU8h)|Zw9I>TaS?)WN&5A19C3#R65a_(W=7*sQxT~wh4%Y1-T)Uy&MEM z`KBM(+H`cDUAz`jk`0F+cqZ<^JH38+Fz_lO2_L=vYEBT(#bHsB%ZO}qK{FSsClv;` zz~2o*W-o{@U72UPhh4Z3<1wUi2OzA)*22uCBXHW6b^#*H$D-@?XfrkK>f#R?p_&rI z?N3u3vr55ajU0}bWc{Wyeu_jj;Rge=L3)ify2lmgp7Yh(9ZP2O;9>I$b`Z(61ftDD zVVG72Lq*L!TA_-eHIZ&X^=aKUjz&P`vXzQOTd8**=qR3s#|77FEyt{7M%lt0CVuWA zTV-UdStF$^_gvrg^MZ)d{A*;lp2GsS*F{QY2oj(@2YhX?;lJJtdD{m60VweO2wP+H zxh}@DY-d#DFC3r*mJjc+@I(IeCrkY$Fmh6v( zmwy>I|D;^jW8)tlWbJ>tpgfARV;b$K@RN^AeMDDQ8RjyzSp3Gu-|BuRSXD_g@NG`; zsY2|we*03WgPbz3HmR+3ZkA_mc6^OprmVacFYCbP^7^Q{HN3DNwm8i~G?UYQvZG|C z-mdw&g>dmoyn(VhjR*QXNS5=HQfSEt0ccp);1P@Sa|PtvrG@5gk^2fKNPD6}^ol?h zDEq9IJ>-5&0FUVM8WCo^(12}#<#6{iS`DBh_LSx?4tos@X+8}llcF*!9e3X7AA8=z z{&l>NNx&}a!;yb|LR{Z#^bIeqHcD0vnkMeyyycsDe7F;_cR5*UkFfhu_Qwfmx+uib zG!qxM810N%9hhnss~Rb+Y!#jy)LWxdRF) z<11ODKe#y;=%~4&;QV!8R(^HL_pxWH{^9g)?h#Sr9`aIPjqmK-v1|H^S?9=;d34-C zi(@VhV?5({zrgud971(^)Z-x_^z4yUmFn?gl<*$G{r%*5h{uyids@_-yM2p0j=)k2 zq9LzmbnN5_d+wD+N4lAH`gGz}CG{1sv~n;TPGoP_zUZ_VHqICml=iF5lQmrza7wEg z5qNNB_`#vP(qn*IVeS%H^Wj$)Ot}oR)5zp`^|FN3jn;vy00IHz@=qd+P;0$SmAJ}3 zmdDc)mZ=^o?}+X4a0esMO|xjHC0oS&R1G~rLQUP<;p%LFaUDwF+SB3!xN5oLtJ7du zwyqk?fA$V3M_GC}Hg8J`+w|FsCMhFlmg_xw7MmTb*x#x!G`9A$XBJ$Fz{7LuFmav` zO$AR*9%pq?M!8rmN7Ppd(y7Sni_d#bSF9IUtYnf7^w! z<3uE2kl1aZy}zp_EWQf@YLaOll-yS|nH$32A9Q_9uf=B!in|))wM!t*cvv?+b$N3- z5Rff^FL^M@`17mU2F}^I`)h-r2qD~WS*q7NrPJGWkp=XWuV$3YYP)1#HT5+sn<@Z_ zLH(z5ICe9nAO7i(dJODlNk?fYntV&L*w5DxtN7#t$4j?LFH?)tk3LL)0`hVxK?b zNJ+n2i*TtfGu65w(Qv;jH@w2qw4$}ouL8* zwGl26r7RHkGq9~$vD@lC$rBvYByoC1cXt}lwJ1Jnbyz#+07eFRBjsWs-f(iTq#C4P z74|J>O(T6S4ERdi$-gY9B>l3T$A8pzdJ#IEqo zW_nxhS}NA6UkA0~r-4y44;-yhG)o_i@Jv5xufVqE+>xeL%vwO4fj^r9twNoO+c%?2xZE zO5nppA%)s=i+oJ$ucSn`{->0y42!Dmx`3oeC@mle(vs3rl7ggkOM{~zIn;0f=@>)= zk&p)IP(T`#R1^UThmsCqXoi&c5X19)->>(&-uXFmoqON0*Shy!Yp$c6hPw3_9?ABo z?Za$x$B&Y4i|frs?oo%{^&4||Q`)?>zoHJEcYH!^+%gL*$HruNjfP(rd(rg(O&++Z zR3s1|F&{&T+g#&*2kzTa8T*DQ`6vipwa8RjE3NqeZP={XWBu#T>v|eK60E74GBgH! z$}`*MQD5voWPwinEwD4c$~>`q0|S3Fl!GC8Fj9B>U`_q0Mzh$`w)$3BuV=2%uMubD zPYxZ^$LqtY5MFe7&u0Y$NXl`A6P@P7pv*DK;9% zsZ}MlmdwJ-k|D0Ic_w`aIOs+zuPbh9{3xX_V<_dlI`&L>`4dnsqCP#kwK(JuiNS6#yp@h_FgL{%dTI{rc4U?|ee6~1>QJogH6O)Z zmh!}6BX*S2TO@v3_oGP>GEH(jzGB(IgdDct=40K~DFRvRBZz&HCKYUj2Mxc;Tb-eI zp!KzRaN5a%776h1M+)RQo27hS6oEf74!k<2UxJHtd--6S4Qkb&z&&iD7Y584B*y7j z@t@{8eyD#nhZx<#oRn?=HV(9|j-NEUcWBXX+NOesFqqOD6j&o%{I+OgTxJmgg7doR_WigE?_%MRqC-^%|hbo-x*wPN&V_zx$u>K81++8 zgCOk4q%~tzV#-i>vWuZUSfo(?3WcS;T6xuBTlIRDG)`m9K1cQ*(U=(Tdzlg2l#+b} zDn+!T49{h zI_cGu5&{1XiNIU`?jwW^{HcVtA5}3H?hN^*Ir-j=m%BdeDc^-qo8lsl=}3{L*7#hc zy(E9T+G#s~_1dGa4FFyeH|lEblqWU1odXr0GJ>*MsV5))5fx6NF@eH@RK>Uif*nf$ z3X46F+^*+%*F>O282rm+#&EpkF&8G!fpyKZYv(kM1*ng}3=C@e^YJ@JFLOhlRJ2oQ zu383BwK;}Dfub=B{?g&LPTu|$(3Fj}Y8AX4Qk&A%m`j5tl6A&}_j$p{fx0sl?&-?) z`8&2s3nRkuEslbN1HU(BQP0+=6zWmqHYPQQFeIivHgig{T$gxzL&43HNV5RWuj9$m zwVHJTx3?byCU*e2(8Ockep>47h$hN_D<}S zuZIg+9>&%*r`BBAYV#Pn^u{>DCR%Oq<6-GatFKTE)e$gFpUDg#FJhUmjpjOZe4_oL zes%;=;j)qI)9($nOe@X?gyuEYlfSY(3@I?ur-f{;K^G}chOT300YMt z>#(#}>1r;`XIb*n+adh@QPyn{Bif?fl68X#;d_gI%HPSWViGb1s%5`kPcCxCF>^(l z>$A)PbpUOSIgaRZ0Id5Z1AsdRUCr{D=B-p4;JN$R6MwM-#njfs2InAAp1NB%?;Jnb z$W#7-Qker``>*w5gnrtRk+)`7)fXwLpYU&)p?l@`v}VDSL6n1-wg|c^mp3Zcu`)7c zVT9+FFXp+{GJ8LDZq=aT{rgJ*3De|D1c-_X&QoA`dWHA8$iJ=#iqa`gYq0!75shSB zkU#cDSu0IcvGFn1&PEBdqEH!}O%#4qAgB$PWDJmFpQHmZdMd&Njd{)5*6T=#=Ick3*ow0hIvf4iSD)Uc9;E za%{;+a}2ZNo85o>Vt74(EBzvhpq9yXyh8Ty;TXxn{wjPE4e(uefTcZ!kebqu05Vjf z(3Mo0&c{o*loblAURu-Ae)LItKH$3xh5(A;^$7}+mU1jSx9U3mR*d-)iHy>KeT&`| zMf0t+SlA@at@m1f1u3^R&eTSbopgp|GBFji5QZILI@;H7|8m$}P`MK#E*0!<$l8rC z2z;m@!PSB$6`>VECL5s&Tp-#)^3J{kt&7|lrTLR4+G;o=zUed{y<5ii;kUJl6|RS4 z&1VZIJ_3N3e%!8HABxJmt`Dj5ln1>cUr_X3@3z(-J=ecUt3h>;*>F32yjy{4pRZUz zWk+gPtQJ?nzg8z3R~km!_3^XsZFF9L^38vSLT`;KQ zzWgE)((ll`yL6o*n_YbY=-?$~!r2ev5j>MgRRlOuoNtRP_^!JeFw!4Q6eZ^hF*EuXG?}qz-@rv!AQUbpG zi!W;2SRyw1d?A+3ii53Ym8XOh(e@{99yoUn!dNlkuB-XQVNtHqE%B4obj`nikfuDw z^MPHP%gbru`wZ8noA<-oB4|vg3^p13$ohL`2))iAL;tP@JcE!KDLQw0qAr-|_vC}@ za{&ldBjsRHmB}zx0lTBoNa+>~k#_>>zJpX9m~}$$WHW z=Y~t(8U*%5lrKRfc~IufwZn~W?c!b~d1w34T8t5Q5T?@*I6kQOb8N|j0Z{Y3P1yz8 zFzC?p*3yS+`xF-6>x$Z&InznF{zhs%TP#$wZ~7Btc6jSgZw%CI7@jt+$y+3NE7~3A zEO}N&_+#;(+|HxAg%?iBwbE4jRg2IaNSZy^cm_Mx2l~}T%^98w{NJoOK7F3*>9h{r z2GpEfq$?6!(P!BjBSV)BcQtua;#2lG9L9NBpR!r>o~Ybdv^~;YRg<%-Q4id_RBJf* z&u*I92*`y<%!OG6Du%UZ8d$sPotG3`jxkV)Uix_>xjc3Y8MqCmI6`vFvwegWw=wS2 zx3ElK@&YcSBX?I|7$fay1l^$F_w~tqwj(WU4w0fnNTg`{C|=oZE4=MAaD1|Z;AwYd5#qUBwlrHpW1Sr;h)wC)xJ1ohN zlYbcn%t9hYix0=y92(LDT4{2rPc^zKK3zQMgEJaRmmw!C>uJD_pI=y%Nu%r_l`1^I zPs@+qi8#nWWPJa#yFf_gMtE7PLcng+QSw^Kmk+XMicD_sx9E%QaR9fcM{9c@THDL= zO|F2S!ok6E*+)(Qco`R$;p(LlcFWvJP&RLPGAYa8GtA)3W*Xi8baZA@fG!0L3&e+Y zDvNiGyQM@*EC)X~2@7OXQ7=p^rshtiU{d1sj z&ty(sMvpXQ=HCVT$a~g(z?@RHZ`?q3Ts$_ih7 zlm3sDqC4_D2pdYo#>yI|6)zA#0gy1JN{+5| z-Hx4--I_9Up?Y?nk4IBZ0-pIV@;d%8qYAH*@o3dfey#LeF}1JvyHuPu@<^qpweQf^ zfYK()gVO}41lTy=PiQ{NclPM0KB$=8HJWXVyb9Y(|bK**ua%n+T?HokEe> z^yX^@QjezD&0jRkHtq~7zFICM(eaz&lR9YI?3~sBG}8%RUrbUPGne)1r2hYNdUx(t z3D_$r+2u`KKQ?5eG}gm?g*p~S%-cBCm^W_Ef85d}Gjak#0I^!UJn;RCP2-{F128I| z!NknewDpVpl{(2WGk_F z5=pOL#>gXP#{<*2W2TGrOZV3~eUd&TnBB|}xMGz{(-lEc%_d>?DD8kCvO@+Fd`c;7 z<>O!H96M*p8{PAAmCV8yi6w&zX0JtQuzYr8t66eL{12q)y^huTzs92je=P2+2a9zC zwi!CyzrsldiR|rnS(S%|{?KPgFlZ!D=w7i}c{sCo&Rv;ijDM5D0hVF`Ctlw+{-Bl? z%&l#YDZOdk`eU=yJmZ+`fF4-`a0rs@ck_iF@4=Uk{``Qjn_R9TL?e-%&j(zJU5|^R zkBT5wW>Jyq!RB^fB|A@du+~^sUnIm`WHPCDe0JR6X-y2gD@b}z+DWRwu8>k1A~@uT;}cstfC`?3u^84bJJa>5Io!Pf&?Ec&c36P zH@wJgOZKrD$6l0ikKsSFp~6~rm8M#nN^Fef>A35+A!{A>cZ~k;XM4E5luA?NTPc(< z`0p~B0>OEuwH>zO2)0O3;5+PsoCagjP8DoHC(0wD^lfZyo*=j2%M$-&xUp@rpQNg#UVPp<5D#p1Z?=l8*B z6(R#>LBI`@e<3_w=I3S4P^WMNS>iWP%X%z!-kF?+Wi-MOHB$dDNH>kP_)ok`nHLy9 z-7I#%v)HrcpK(PmCrEZi5V$iMI;RHheGu=Y_*7^TJ>eYEV(|2{D(9$0E@%e)IuIm2 jy#IR0hyTNvYsZ+$M})*KoSOI;;73_OUA{#2QQ&_7{<_iD From 91a9dfd9830f600f89c90f42bb9d89e64cbe5e89 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 2 Sep 2018 21:52:59 +0300 Subject: [PATCH 672/769] invoke dns encryption from main playbook instead of meta-dependencies (#1097) --- roles/vpn/meta/main.yml | 5 ----- server.yml | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/roles/vpn/meta/main.yml b/roles/vpn/meta/main.yml index 5f86e875..ed97d539 100644 --- a/roles/vpn/meta/main.yml +++ b/roles/vpn/meta/main.yml @@ -1,6 +1 @@ --- - -dependencies: - - role: dns_encryption - tags: dns_encryption - when: dns_encryption diff --git a/server.yml b/server.yml index 459dd63d..e7e4ad2a 100644 --- a/server.yml +++ b/server.yml @@ -9,6 +9,9 @@ roles: - role: common + - role: dns_encryption + when: dns_encryption + tags: dns_encryption - role: dns_adblocking when: algo_local_dns tags: dns_adblocking From cbe57991db8236a1f3b6cef890a33b875ea6ed3c Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 2 Sep 2018 21:54:06 +0300 Subject: [PATCH 673/769] Update docs (#1089) --- docs/client-macos-wireguard.md | 33 ++++++++++++ docs/faq.md | 5 ++ docs/troubleshooting.md | 91 +++++++++++++++++++++++++++++++++- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 docs/client-macos-wireguard.md diff --git a/docs/client-macos-wireguard.md b/docs/client-macos-wireguard.md new file mode 100644 index 00000000..0d1db781 --- /dev/null +++ b/docs/client-macos-wireguard.md @@ -0,0 +1,33 @@ +# Using MacOS as a Client with WireGuard + +## Install WireGuard + +To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from MacOS + +``` +# Install the wireguard-go userspace driver +brew install wireguard-tools +``` + +## Locate the Config File + +The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your device. + +## Configure WireGuard + +Finally, install the config file on your client as `/usr/local/etc/wireguard/wg0.conf` and start WireGuard: + +``` +# Install the config file to the WireGuard configuration directory on your MacOS device +mkdir /usr/local/etc/wireguard/ +cp .conf /usr/local/etc/wireguard/wg0.conf + +# Start the WireGuard VPN: +sudo wg-quick up wg0 + +# Verify the connection to the Algo VPN: +wg + +# See that your client is using the IP address of your Algo VPN: +curl ipv4.icanhazip.com +``` diff --git a/docs/faq.md b/docs/faq.md index b55a911e..00b44f83 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,6 +10,7 @@ * [Where did the name "Algo" come from?](#where-did-the-name-algo-come-from) * [Can DNS filtering be disabled?](#can-dns-filtering-be-disabled) * [Wasn't IPSEC backdoored by the US government?](#wasnt-ipsec-backdoored-by-the-us-government) +* [What inbound ports are used?](#what-inbound-ports-are-used) ## Has Algo been audited? @@ -70,3 +71,7 @@ No. > It's interesting that the bug was fixed without an advisory (oh to be a fly on the wall on ICB that day; Theo had a, um, a, "way" with his dev team). On the other hand, we don't know what releases of OpenBSD actually had the bug right now. > > It seems vanishingly unlikely that there could have been anything deliberate about this series of changes. You are unlikely to find anyone who will impugn Angelos. Meanwhile, the diffs tell exactly the opposite of the story that Greg Perry told. + +## What inbound ports are used? + +You should only need 22/TCP, 500/UDP, and 4500/UDP. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e6717b7a..53bacb11 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -6,11 +6,15 @@ * [Error: "fatal error: 'openssl/opensslv.h' file not found"](#error-fatal-error-opensslopensslvh-file-not-found) * [Error: "TypeError: must be str, not bytes"](#error-typeerror-must-be-str-not-bytes) * [Error: "ansible-playbook: command not found"](#error-ansible-playbook-command-not-found) + * [Error: "Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION](#could-not-fetch-url--tlsv1_alert_protocol_version) * [Bad owner or permissions on .ssh](#bad-owner-or-permissions-on-ssh) * [The region you want is not available](#the-region-you-want-is-not-available) * [AWS: SSH permission denied with an ECDSA key](#aws-ssh-permission-denied-with-an-ecdsa-key) * [AWS: "Deploy the template" fails with CREATE_FAILED](#aws-deploy-the-template-fails-with-create_failed) + * [AWS: not authorized to perform: cloudformation:UpdateStack](#aws-not-authorized-to-perform-cloudformationupdatestack) * [DigitalOcean: error tagging resource 'xxxxxxxx': param is missing or the value is empty: resources](#digitalocean-error-tagging-resource) + * [Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid](#windows-the-value-of-parameter-linuxconfigurationsshpublickeyskeydata-is-invalid) + * [Docker: Failed to connect to the host via ssh](#docker-failed-to-connect-to-the-host-via-ssh) * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -21,6 +25,7 @@ * [Various websites appear to be offline through the VPN](#various-websites-appear-to-be-offline-through-the-vpn) * [Clients appear stuck in a reconnection loop](#clients-appear-stuck-in-a-reconnection-loop) * ["Error 809" or IKE_AUTH requests that never make it to the server](#error-809-or-ike_auth-requests-that-never-make-it-to-the-server) + * [Windows: Parameter is incorrect](#windows-parameter-is-incorrect) * [I have a problem not covered here](#i-have-a-problem-not-covered-here) ## Installation Problems @@ -150,7 +155,7 @@ In order to fix this issue, delete the `algo.pem` and `algo.pem.pub` keys from y ### AWS: "Deploy the template fails" with CREATE_FAILED -You tried to deploy to Algo to AWS and you received an error like this one: +You tried to deploy Algo to AWS and you received an error like this one: ``` TASK [cloud-ec2 : Make a cloudformation template] ****************************** @@ -166,7 +171,7 @@ In many cases, failed deployments are the result of [service limits](http://docs ### DigitalOcean: error tagging resource -You tried to deploy to Algo to DigitalOcean and you received an error like this one: +You tried to deploy Algo to DigitalOcean and you received an error like this one: ``` TASK [cloud-digitalocean : Tag the droplet] ************************************ @@ -183,6 +188,65 @@ The error is caused because Digital Ocean changed its API to treat the tag argum 5. Finally run `doctl compute tag list` to make sure that the tag has been deleted 6. Run algo as directed +### Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid + +You tried to deploy Algo from Windows and you received an error like this one: + +``` +TASK [cloud-azure : Create an instance]. +fatal: [localhost]: FAILED! => {"changed": false, +"msg": "Error creating or updating virtual machine AlgoVPN - Azure Error: +InvalidParameter\n +Message: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid.\n +Target: linuxConfiguration.ssh.publicKeys.keyData"} +``` + +This is related to [the chmod issue](https://github.com/Microsoft/WSL/issues/81) inside /mnt directory which is NTFS. The fix is to place Algo outside of /mnt directory. + +### Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION + +You tried to install Algo and you received an error like this one: + +``` +Could not fetch URL https://pypi.python.org/simple/secretstorage/: There was a problem confirming the ssl certificate: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:590) - skipping + Could not find a version that satisfies the requirement SecretStorage<3 (from -r requirements.txt (line 2)) (from versions: ) +No matching distribution found for SecretStorage<3 (from -r requirements.txt (line 2)) +``` + +It's time to upgrade your python + +`brew upgrade python2` + +You can also download python 2.7.x from python.org + +### Docker: Failed to connect to the host via ssh + +You tried to deploy Algo from Docker and you received an error like this one: + +``` +Failed to connect to the host via ssh: +Warning: Permanently added 'xxx.xxx.xxx.xxx' (ECDSA) to the list of known hosts.\r\n +Control socket connect(/root/.ansible/cp/6d9d22e981): Connection refused\r\n +Failed to connect to new control master\r\n +``` + +You need to add the following to the ansible.cfg in repo root: + +``` +[ssh_connection] +control_path_dir=/dev/shm/ansible_control_path +``` + +### AWS: not authorized to perform: cloudformation:UpdateStack + +You tried to deploy Algo to AWS and you received an error like this one: + +``` +TASK [cloud-ec2 : Deploy the template] ***************************************** +fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "User: arn:aws:iam::082851645362:user/algo is not authorized to perform: cloudformation:UpdateStack on resource: arn:aws:cloudformation:us-east-1:082851645362:stack/algo/*"} +``` + +This error indicates you already have Algo deployed to Cloudformation. Need to [delete it](cloud-amazon-ec2.md#cleanup) first, then re-deploy. ## Connection Problems @@ -278,6 +342,29 @@ Then rerun the dependency installation explicitly using python 2.7 python2.7 -m virtualenv --python=`which python2.7` env && source env/bin/activate && python2.7 -m pip install -U pip && python2.7 -m pip install -r requirements.txt ``` +### Windows: Parameter is incorrect + +The problem may happen if you recently moved to a new server, where you have Algo VPN. + +1. Clear the Networking caches: + - Run CDM (click windows start menu, type 'cmd', right click on 'Command Prompt' and select "Run as Administrator"). + - Type the commands below: + ``` + netsh int ip reset + netsh int ipv6 reset + netsh winsock reset + ``` + +3. Restart your computer +4. Reset Device Manager adaptors: + - Open Device Manager + - Find Network Adapters + - Uninstall WAN Miniport drivers (IKEv2, IP, IPv6, etc) + - Click Action > Scan for hardware changes + - The adapters you just uninstalled should come back + +The VPN connection should work again + ## I have a problem not covered here If you have an issue that you cannot solve with the guidance here, [join our Gitter](https://gitter.im/trailofbits/algo) and ask for help. If you think you found a new issue in Algo, [file an issue](https://github.com/trailofbits/algo/issues/new). From 244a698531837b04c79a46f56118a0110c1f45d1 Mon Sep 17 00:00:00 2001 From: in-in Date: Sun, 2 Sep 2018 22:22:24 +0300 Subject: [PATCH 674/769] improve readability (#1085) --- docs/client-linux-wireguard.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/client-linux-wireguard.md b/docs/client-linux-wireguard.md index 123ab76e..3430959c 100644 --- a/docs/client-linux-wireguard.md +++ b/docs/client-linux-wireguard.md @@ -4,11 +4,13 @@ To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from an Ubuntu Server 16.04 (Xenial) or 18.04 (Bionic) client, first install WireGuard on the client: -``` +```shell # Add the WireGuard repository: sudo add-apt-repository ppa:wireguard/wireguard + # Update the list of available packages (not necessary on Bionic): sudo apt update + # Install the tools and kernel module: sudo apt install wireguard ``` @@ -29,20 +31,25 @@ Use the IP address shown on the `DNS =` line (for most, this will be `172.16.0.1 Finally, install the config file on your client as `/etc/wireguard/wg0.conf` and start WireGuard: -``` +```shell # Install the config file to the WireGuard configuration directory on your # Bionic or Xenial client: sudo install -o root -g root -m 600 .conf /etc/wireguard/wg0.conf + # Start the WireGuard VPN: sudo systemctl start wg-quick@wg0 + # Check that it started properly: sudo systemctl status wg-quick@wg0 + # Verify the connection to the Algo VPN: sudo wg + # See that your client is using the IP address of your Algo VPN: curl ipv4.icanhazip.com + # Optionally configure the connection to come up at boot time: sudo systemctl enable wg-quick@wg0 ``` -(If your Linux distribution does not use `systemd`, you can bring up WireGuard with `sudo wg-quick up wg0`). \ No newline at end of file +(If your Linux distribution does not use `systemd`, you can bring up WireGuard with `sudo wg-quick up wg0`). From d95df710a55d664400ee2802fd5d4b2322ac1340 Mon Sep 17 00:00:00 2001 From: David Myers Date: Sun, 2 Sep 2018 15:26:06 -0400 Subject: [PATCH 675/769] Add an unattended reboot option (#1082) --- config.cfg | 9 +++++++++ roles/common/tasks/unattended-upgrades.yml | 8 ++++++++ roles/common/templates/60unattended-reboot.j2 | 2 ++ 3 files changed, 19 insertions(+) create mode 100644 roles/common/templates/60unattended-reboot.j2 diff --git a/config.cfg b/config.cfg index b5bbb9ca..6156031a 100644 --- a/config.cfg +++ b/config.cfg @@ -56,6 +56,15 @@ dns_servers: # IP address for the local dns resolver local_service_ip: 172.16.0.1 +# Your Algo server will automatically install security updates. Some updates +# require a reboot to take effect but your Algo server will not reboot itself +# automatically unless you change 'enabled' below from 'false' to 'true', in +# which case a reboot will take place if necessary at the time specified (as +# HH:MM) in the time zone of your Algo server. The default time zone is UTC. +unattended_reboot: + enabled: false + time: 06:00 + pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" diff --git a/roles/common/tasks/unattended-upgrades.yml b/roles/common/tasks/unattended-upgrades.yml index 378c16e3..d0beae0a 100644 --- a/roles/common/tasks/unattended-upgrades.yml +++ b/roles/common/tasks/unattended-upgrades.yml @@ -19,3 +19,11 @@ owner: root group: root mode: 0644 + +- name: Unattended reboots configured + template: + src: 60unattended-reboot.j2 + dest: /etc/apt/apt.conf.d/60unattended-reboot + owner: root + group: root + mode: 0644 diff --git a/roles/common/templates/60unattended-reboot.j2 b/roles/common/templates/60unattended-reboot.j2 new file mode 100644 index 00000000..6af49126 --- /dev/null +++ b/roles/common/templates/60unattended-reboot.j2 @@ -0,0 +1,2 @@ +Unattended-Upgrade::Automatic-Reboot "{{ unattended_reboot.enabled|lower }}"; +Unattended-Upgrade::Automatic-Reboot-Time "{{ unattended_reboot.time }}"; From 4c70b71df509bbbc0ac7c6824a6ea57d84e65e33 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Thu, 6 Sep 2018 14:04:23 -0400 Subject: [PATCH 676/769] Fix spacing in congrats message (#1104) The spacing of several lines in the congrats message has been off. Here's the congrats output with this fix: ``` ok: [54.85.244.8] => { "msg": [ [ "\"# Congratulations! #\"", "\"# Your Algo server is running. #\"", "\"# Config files and certificates are in the ./configs/ directory. #\"", "\"# Go to https://whoer.net/ after connecting #\"", "\"# and ensure that all your traffic passes through the VPN. #\"", "\"# Local DNS resolver 172.16.0.1 #\"", "" ], " \"# The p12 and SSH keys password for new users is CR2qzRcA #\"\n", " \"# The CA key password is ed0fd57e7d355af08d12ccdbfd3f5931 #\"\n", " \"# Shell access: ssh -i configs/algo.pem ubuntu@54.85.244.8 #\"\n" ] } ``` --- config.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.cfg b/config.cfg index 6156031a..f9722514 100644 --- a/config.cfg +++ b/config.cfg @@ -79,11 +79,11 @@ congrats: "# Config files and certificates are in the ./configs/ directory. #" "# Go to https://whoer.net/ after connecting #" "# and ensure that all your traffic passes through the VPN. #" - "# Local DNS resolver {{ local_service_ip }} #" + "# Local DNS resolver {{ local_service_ip }} #" p12_pass: | - "# The p12 and SSH keys password for new users is {{ p12_export_password }} #" + "# The p12 and SSH keys password for new users is {{ p12_export_password }} #" ca_key_pass: | - "# The CA key password is {{ CA_password }} #" + "# The CA key password is {{ CA_password }} #" ssh_access: | "# Shell access: ssh -i {{ ansible_ssh_private_key_file|default(omit) }} {{ ansible_ssh_user|default(omit) }}@{{ ansible_ssh_host|default(omit) }} #" From 76a8fe35db0448366a777c285cb0620747de4e32 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Fri, 7 Sep 2018 06:04:20 -0400 Subject: [PATCH 677/769] Document AWS disk encryption flag in config.cfg (#1102) This is to better document the "encryption" flag for those who are interested in full disk encryption on AWS. Recently on running the script, I also found the minimum permissions documented at https://github.com/trailofbits/algo/blob/master/docs/deploy-from-ansible.md weren't enough; "ec2:CopyImage" is also required. Not sure if you'd rather have this documented in the AWS docs instead, and not sure if you want "ec2:CopyImage" added to the default minimum required permissions. I can do either if you'd prefer. --- config.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config.cfg b/config.cfg index f9722514..c838a640 100644 --- a/config.cfg +++ b/config.cfg @@ -103,6 +103,12 @@ cloud_providers: digitalocean: size: s-1vcpu-1gb image: "ubuntu-18-04-x64" + # Change the encrypted flag to "true" to enable AWS volume encryption, for encryption of data at rest. + # Warning: the Algo script will take approximately 6 minutes longer to complete. + # Also note that the documented AWS minimum permissions aren't sufficient. + # You will have to edit the AWS user policy documented at + # https://github.com/trailofbits/algo/blob/master/docs/cloud-amazon-ec2.md to also allow "ec2:CopyImage". + # See https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage-edit.html ec2: encrypted: false size: t2.micro From 65b02396253d0eadd48af570f9e9086cc4637a62 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 7 Sep 2018 10:25:57 -0400 Subject: [PATCH 678/769] Display the invocation environment to aid debugging (#1108) --- algo-showenv.sh | 84 +++++++++++++++++++++++++++++++++++++++++ playbooks/cloud-pre.yml | 15 ++++++++ 2 files changed, 99 insertions(+) create mode 100755 algo-showenv.sh diff --git a/algo-showenv.sh b/algo-showenv.sh new file mode 100755 index 00000000..6085c407 --- /dev/null +++ b/algo-showenv.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# +# Print information about Algo's invocation environment to aid in debugging. +# This is normally called from Ansible right before a deployment gets underway. + +# Skip printing this header if we're just testing with no arguments. +if [[ $# -gt 0 ]]; then + echo "" + echo "--> Please include the following block of text when reporting issues:" + echo "" +fi + +if [[ ! -f ./algo ]]; then + echo "This should be run from the top level Algo directory" +fi + +# Determine the operating system. +case "$(uname -s)" in + Linux) + OS="Linux ($(uname -r) $(uname -v))" + if [[ -f /etc/os-release ]]; then + # shellcheck disable=SC1091 + # I hope this isn't dangerous. + . /etc/os-release + if [[ ${PRETTY_NAME} ]]; then + OS="${PRETTY_NAME}" + elif [[ ${NAME} ]]; then + OS="${NAME} ${VERSION}" + fi + fi + STAT="stat -c %y" + ;; + Darwin) + OS="$(sw_vers -productName) $(sw_vers -productVersion)" + STAT="stat -f %Sm" + ;; + *) + OS="Unknown" + ;; +esac + +# Determine if virtualization is being used with Linux. +VIRTUALIZED="" +if [[ -x $(command -v systemd-detect-virt) ]]; then + DETECT_VIRT="$(systemd-detect-virt)" + if [[ ${DETECT_VIRT} != "none" ]]; then + VIRTUALIZED=" (Virtualized: ${DETECT_VIRT})" + fi +fi + +echo "Algo running on: ${OS}${VIRTUALIZED}" + +# Determine the currentness of the Algo software. +if [[ -d .git && -x $(command -v git) ]]; then + ORIGIN="$(git remote get-url origin)" + COMMIT="$(git log --max-count=1 --oneline --no-decorate --no-color)" + if [[ ${ORIGIN} == "https://github.com/trailofbits/algo.git" ]]; then + SOURCE="clone" + else + SOURCE="fork" + fi + echo "Created from git ${SOURCE}. Last commit: ${COMMIT}" +elif [[ -f LICENSE && ${STAT} ]]; then + CREATED="$(${STAT} LICENSE)" + echo "ZIP file created: ${CREATED}" +fi + +# The Python version might be useful to know. +if [[ -x ./env/bin/python ]]; then + ./env/bin/python --version 2>&1 +elif [[ -f ./algo ]]; then + echo "env/bin/python not found: has 'python -m virtualenv ...' been run?" +fi + +# Just print out all command line arguments, which are expected +# to be Ansible variables. +if [[ $# -gt 0 ]]; then + echo "Runtime variables:" + for VALUE in "$@"; do + echo " ${VALUE}" + done +fi + +exit 0 diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index da08b357..b40f6c85 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -1,4 +1,19 @@ --- +- name: Display the invocation environment + local_action: + module: shell + ./algo-showenv.sh \ + 'algo_provider "{{ algo_provider }}"' \ + 'algo_ondemand_cellular "{{ algo_ondemand_cellular }}"' \ + 'algo_ondemand_wifi "{{ algo_ondemand_wifi }}"' \ + 'algo_ondemand_wifi_exclude "{{ algo_ondemand_wifi_exclude }}"' \ + 'algo_local_dns "{{ algo_local_dns }}"' \ + 'algo_ssh_tunneling "{{ algo_ssh_tunneling }}"' \ + 'algo_windows "{{ algo_windows }}"' \ + 'wireguard_enabled "{{ wireguard_enabled }}"' \ + 'dns_encryption "{{ dns_encryption }}"' \ + > /dev/tty + - name: Generate the SSH private key openssl_privatekey: path: "{{ SSH_keys.private }}" From 57fb2ec3470f64be8785e4c7d77dc460b25b8db4 Mon Sep 17 00:00:00 2001 From: ctrlaltreboot <1124760+ctrlaltreboot@users.noreply.github.com> Date: Sat, 8 Sep 2018 23:38:49 +1000 Subject: [PATCH 679/769] Update client-windows.md (#1099) Correct command would be ```powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 Add``` --- docs/client-windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 6e071cf1..91dfd9c4 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -8,7 +8,7 @@ To install automatically, use the generated user Powershell script. 2. Open Powershell as Administrator. 3. Run the following command: ```powershell -powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 Add ``` 4. The command has help information available. To view its full help, run this from Powershell: ```powershell From df4b3f620290f6b762e0a67501b5b9984f6950c2 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Sat, 8 Sep 2018 09:39:53 -0400 Subject: [PATCH 680/769] Update Win10 client docs for non-admin accounts (#1093) * Update client-windows.md Allows non-admin accounts to use the VPN as per #983 and #994. Fix was also documented here https://www.bountysource.com/issues/49259904-windows-10-powershell-and-priv-nonpriv-account-issues * Update client-windows.md --- docs/client-windows.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 91dfd9c4..77ba3c6f 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -1,6 +1,6 @@ # Windows client manual setup -## Automatic installtion +## Automatic installation To install automatically, use the generated user Powershell script. @@ -27,6 +27,8 @@ Set-ExecutionPolicy Unrestricted -Scope CurrentUser 4. In the same window, run the necessary commands to install the certificates and create the VPN configuration. Note the lines at the top defining the VPN address, USER.p12 file location, and CA certificate location - change those lines to the IP address of your Algo server and the location you saved those two files. Also note that it will prompt for the "User p12 password", which is printed at the end of a successful Algo deployment. +If you have more than one account on your Windows 10 machine (e.g. one with administrator privileges and one without) and would like to have the VPN connection available to all users, then insert the line `AllUserConnection = $true` after `$EncryptionLevel = "Required"`. + ```powershell $VpnServerAddress = "1.2.3.4" $UserP12Path = "$Home\Downloads\USER.p12" From 5e7f134005fb371347dcb1c86d5932fec45ae820 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 14 Sep 2018 16:09:46 +0300 Subject: [PATCH 681/769] Update issue templates (#1114) * Update issue templates * Delete ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 37 ----------------------- .github/ISSUE_TEMPLATE/bug_report.md | 32 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 17 +++++++++++ 3 files changed, 49 insertions(+), 37 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7a8982df..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,37 +0,0 @@ -### OS / Environment (where do you run Algo on) - - -``` -PUT THE OUTPUT HERE -``` - -### Cloud Provider (where do you deploy Algo to) - - -``` -PUT THE OUTPUT HERE -``` - -### Summary of the problem - - - - -### Steps to reproduce the behavior - - -1. Do this.. -2. Do that.. -3. - -### Full log - - -``` -PUT THE OUTPUT HERE -``` diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..cd9b4c6a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: +1. Do this.. +2. Do that.. +3. .. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Additional context** + +Add any other context about the problem here. + +**Full log** + + + +``` +PUT THE OUTPUT HERE +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..066b2d92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 4e5103986c154dc3f13c22c30d8f2a3cff03b686 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 14 Sep 2018 16:22:27 +0300 Subject: [PATCH 682/769] Create PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..74118487 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,29 @@ + + +## Description + + +## Motivation and Context + + + +## How Has This Been Tested? + + + + +## Types of changes + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist: + + +- [ ] I have read the **CONTRIBUTING** document. +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. From 4a42fbea35167de1b139dad95405189f3b3e8e69 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 17 Sep 2018 03:19:29 +0300 Subject: [PATCH 683/769] Move to the ARM deployment schema (#1107) --- CHANGELOG.md | 4 + config.cfg | 6 +- roles/cloud-azure/defaults/main.yml | 2 +- roles/cloud-azure/files/deployment.json | 209 ++++++++++++++++++++++++ roles/cloud-azure/tasks/main.yml | 135 +++------------ roles/cloud-azure/tasks/prompts.yml | 10 +- 6 files changed, 245 insertions(+), 121 deletions(-) create mode 100644 roles/cloud-azure/files/deployment.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f566a4..417b757d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7 Sep 2018 +### Changed +- Azure: Deployment via Azure Resource Manager + ## 27 Aug 2018 ### Changed - Large refactor to support Ansible 2.5. [Details](https://github.com/trailofbits/algo/pull/976) diff --git a/config.cfg b/config.cfg index c838a640..0967d260 100644 --- a/config.cfg +++ b/config.cfg @@ -95,11 +95,7 @@ SSH_keys: cloud_providers: azure: size: Basic_A0 - image: - offer: UbuntuServer - publisher: Canonical - sku: '18.04-LTS' - version: latest + image: 18.04-LTS digitalocean: size: s-1vcpu-1gb image: "ubuntu-18-04-x64" diff --git a/roles/cloud-azure/defaults/main.yml b/roles/cloud-azure/defaults/main.yml index 9170a157..cd5301d2 100644 --- a/roles/cloud-azure/defaults/main.yml +++ b/roles/cloud-azure/defaults/main.yml @@ -1,5 +1,5 @@ --- -azure_regions: > +_azure_regions: > [ { "displayName": "East Asia", diff --git a/roles/cloud-azure/files/deployment.json b/roles/cloud-azure/files/deployment.json new file mode 100644 index 00000000..646ea8a1 --- /dev/null +++ b/roles/cloud-azure/files/deployment.json @@ -0,0 +1,209 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "AlgoServerName": { + "type": "string" + }, + "sshKeyData": { + "type": "string" + }, + "location": { + "type": "string" + }, + "WireGuardPort": { + "type": "int" + }, + "vmSize": { + "type": "string" + }, + "imageReferenceSku": { + "type": "string" + } + }, + "variables": { + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('AlgoServerName'))]", + "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', parameters('AlgoServerName'))]" + }, + "resources": [ + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/networkSecurityGroups", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "securityRules": [ + { + "name": "AllowSSH", + "properties": { + "description": "Locks inbound down to ssh default port 22.", + "protocol": "Tcp", + "sourcePortRange": "*", + "destinationPortRange": "22", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 100, + "direction": "Inbound" + } + }, + { + "name": "AllowIPSEC500", + "properties": { + "description": "Allow UDP to port 500", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "500", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 110, + "direction": "Inbound" + } + }, + { + "name": "AllowIPSEC4500", + "properties": { + "description": "Allow UDP to port 4500", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "4500", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 120, + "direction": "Inbound" + } + }, + { + "name": "AllowWireGuard", + "properties": { + "description": "Locks inbound down to ssh default port 22.", + "protocol": "Udp", + "sourcePortRange": "*", + "destinationPortRange": "[parameters('WireGuardPort')]", + "sourceAddressPrefix": "*", + "destinationAddressPrefix": "*", + "access": "Allow", + "priority": 130, + "direction": "Inbound" + } + } + ] + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "publicIPAllocationMethod": "Static" + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/virtualNetworks", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.10.0.0/16" + ] + }, + "subnets": [ + { + "name": "[parameters('AlgoServerName')]", + "properties": { + "addressPrefix": "10.10.0.0/24" + } + } + ] + } + }, + { + "apiVersion": "2015-06-15", + "type": "Microsoft.Network/networkInterfaces", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkSecurityGroups/', parameters('AlgoServerName'))]", + "[concat('Microsoft.Network/publicIPAddresses/', parameters('AlgoServerName'))]", + "[concat('Microsoft.Network/virtualNetworks/', parameters('AlgoServerName'))]" + ], + "properties": { + "networkSecurityGroup": { + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('AlgoServerName'))]" + }, + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('AlgoServerName'))]" + }, + "subnet": { + "id": "[variables('subnet1Ref')]" + } + } + } + ] + } + }, + { + "apiVersion": "2016-04-30-preview", + "type": "Microsoft.Compute/virtualMachines", + "name": "[parameters('AlgoServerName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[concat('Microsoft.Network/networkInterfaces/', parameters('AlgoServerName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[parameters('vmSize')]" + }, + "osProfile": { + "computerName": "[parameters('AlgoServerName')]", + "adminUsername": "ubuntu", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "ssh": { + "publicKeys": [ + { + "path": "/home/ubuntu/.ssh/authorized_keys", + "keyData": "[parameters('sshKeyData')]" + } + ] + } + } + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "[parameters('imageReferenceSku')]", + "version": "latest" + }, + "osDisk": { + "createOption": "FromImage" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('AlgoServerName'))]" + } + ] + } + } + } + ], + "outputs": { + "publicIPAddresses": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',parameters('AlgoServerName')),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", + } + } +} diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 682fcb3c..27e2defc 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -4,123 +4,38 @@ import_tasks: prompts.yml - set_fact: - resource_group: "Algo_{{ region }}" - secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" - tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" - client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" - subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ azure_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} - - name: Create a resource group - azure_rm_resourcegroup: + - name: Create AlgoVPN Server + azure_rm_deployment: + state: present + deployment_name: "AlgoVPN-{{ algo_server_name }}" + template: "{{ lookup('file', 'deployment.json') }}" secret: "{{ secret }}" tenant: "{{ tenant }}" client_id: "{{ client_id }}" subscription_id: "{{ subscription_id }}" - name: "{{ resource_group }}" - location: "{{ region }}" - tags: - Environment: Algo - - - name: Create a virtual network - azure_rm_virtualnetwork: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_net - address_prefixes: "10.10.0.0/16" - tags: - Environment: Algo - - - name: Create a security group - azure_rm_securitygroup: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: AlgoSecGroup - purge_rules: yes - rules: - - name: AllowSSH - protocol: Tcp - destination_port_range: 22 - access: Allow - priority: 100 - direction: Inbound - - name: AllowIPSEC500 - protocol: Udp - destination_port_range: 500 - access: Allow - priority: 110 - direction: Inbound - - name: AllowIPSEC4500 - protocol: Udp - destination_port_range: 4500 - access: Allow - priority: 120 - direction: Inbound - - name: AllowWireGuard - protocol: Udp - destination_port_range: "{{ wireguard_port }}" - access: Allow - priority: 130 - direction: Inbound - - - name: Create a subnet - azure_rm_subnet: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - name: algo_subnet - address_prefix: "10.10.0.0/24" - virtual_network: algo_net - security_group_name: AlgoSecGroup - tags: - Environment: Algo - - - name: Create an instance - azure_rm_virtualmachine: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group: "{{ resource_group }}" - admin_username: ubuntu - virtual_network: algo_net - name: "{{ azure_server_name }}" - ssh_password_enabled: false - vm_size: "{{ cloud_providers.azure.size }}" - tags: - Environment: Algo - ssh_public_keys: - - { path: "/home/ubuntu/.ssh/authorized_keys", key_data: "{{ lookup('file', '{{ SSH_keys.public }}') }}" } - image: "{{ cloud_providers.azure.image }}" - register: azure_rm_virtualmachine - - # To-do: Add error handling - if vm_size requested is not available, can we fall back to another, ideally with a prompt? + resource_group_name: "AlgoVPN-{{ algo_server_name }}" + parameters: + AlgoServerName: + value: "{{ algo_server_name }}" + sshKeyData: + value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + location: + value: "{{ algo_region }}" + WireGuardPort: + value: "{{ wireguard_port }}" + vmSize: + value: "{{ cloud_providers.azure.size }}" + imageReferenceSku: + value: "{{ cloud_providers.azure.image }}" + register: azure_rm_deployment - set_fact: - ip_address: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}" - networkinterface_name: "{{ azure_rm_virtualmachine.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].name }}" - - - name: Ensure the network interface includes all required parameters - azure_rm_networkinterface: - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - name: "{{ networkinterface_name }}" - resource_group: "{{ resource_group }}" - virtual_network_name: algo_net - subnet_name: algo_subnet - security_group_name: AlgoSecGroup - - - set_fact: - cloud_instance_ip: "{{ ip_address }}" + cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" ansible_ssh_user: ubuntu rescue: diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml index aadffd61..28d42521 100644 --- a/roles/cloud-azure/tasks/prompts.yml +++ b/roles/cloud-azure/tasks/prompts.yml @@ -48,20 +48,20 @@ - block: - name: Set facts about the regions set_fact: - aws_regions: "{{ azure_regions | sort(attribute='region_name') }}" + azure_regions: "{{ _azure_regions|from_json | sort(attribute='name') }}" - name: Set the default region set_fact: default_region: >- - {% for r in aws_regions %} - {%- if r['region_name'] == "us-east-1" %}{{ loop.index }}{% endif %} + {% for r in azure_regions %} + {%- if r['name'] == "eastus" %}{{ loop.index }}{% endif %} {%- endfor %} - pause: prompt: | What region should the server be located in? - {% for r in aws_regions %} - {{ loop.index }}. {{ r['region_name'] }} + {% for r in azure_regions %} + {{ loop.index }}. {{ r['displayName'] }} {% endfor %} Enter the number of your desired region From 14234344ebe5d5d687faebbc6fd0c99e520fa4a0 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 18 Sep 2018 02:43:41 -0500 Subject: [PATCH 684/769] Use gateway ip address for wireguard interface (#1115) --- roles/wireguard/templates/server.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index 17b388fc..d9468de4 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -1,5 +1,5 @@ [Interface] -Address = {{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }} +Address = {{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }} {% endif %} ListenPort = {{ wireguard_port }} From 8f090a36f8a619b6293719f1624c8d11a4cb4c61 Mon Sep 17 00:00:00 2001 From: Mike Myers <30631532+mike-myers-tob@users.noreply.github.com> Date: Tue, 18 Sep 2018 00:47:07 -0700 Subject: [PATCH 685/769] Fix minor typos in Amazon EC2 setup documentation. (#1116) --- docs/cloud-amazon-ec2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cloud-amazon-ec2.md b/docs/cloud-amazon-ec2.md index 36c51359..1e81988f 100644 --- a/docs/cloud-amazon-ec2.md +++ b/docs/cloud-amazon-ec2.md @@ -6,7 +6,7 @@ Creating an Amazon AWS account requires giving Amazon a phone number that can re ### Select an EC2 plan -The cheapest EC2 plan you can choose is the "Free Plan" a.k.a. the "AWS Free Tier." It is only available to new AWS customers, it has limits on usage, and is converts to standard pricing after 12 months (the "introductory period"). After you exceed the usage limits, after the 12 month period, or if you are an existing AWS customer, then you will pay standard pay-as-you-go service prices. +The cheapest EC2 plan you can choose is the "Free Plan" a.k.a. the "AWS Free Tier." It is only available to new AWS customers, it has limits on usage, and it converts to standard pricing after 12 months (the "introductory period"). After you exceed the usage limits, after the 12 month period, or if you are an existing AWS customer, then you will pay standard pay-as-you-go service prices. *Note*: Your Algo instance will not stop working when you hit the bandwidth limit, you will just start accumulating service charges on your AWS account. @@ -22,7 +22,7 @@ Here, you have the policy editor. Switch to the JSON tab and copy-paste over the ### Set up an AWS user -In the AWS console, find the users (“Identiy and Access Management”, a.k.a. IAM users) menu: click Services > IAM. +In the AWS console, find the users (“Identity and Access Management”, a.k.a. IAM users) menu: click Services > IAM. Activate multi-factor authentication (MFA) on your root account. The simplest choice is the mobile app "Google Authenticator." A hardware U2F token is ideal (less prone to a phishing attack), but a TOTP authenticator like this is good enough. From eb2224cde1ea7c739315af1824a9b373fa24be81 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 21 Sep 2018 20:05:11 +0300 Subject: [PATCH 686/769] install generic linux headers (#1124) --- roles/common/defaults/main.yml | 2 ++ roles/common/tasks/ubuntu.yml | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 roles/common/defaults/main.yml diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 00000000..f358d3e1 --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,2 @@ +--- +install_headers: true diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index fee3af42..9c6e6a5b 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -108,7 +108,7 @@ - coreutils - iptables-persistent - cgroup-tools - - "openssl{% if install_headers|default(true)|bool %},linux-headers-{{ ansible_kernel }}{% endif %}" + - openssl sysctl: - item: net.ipv4.ip_forward value: 1 @@ -125,3 +125,12 @@ - "{{ tools|default([]) }}" tags: - always + +- name: Install headers + apt: + name: "{{ item }}" + state: present + when: install_headers + with_items: + - linux-headers-generic + - "linux-headers-{{ ansible_kernel }}" From aa318bff18834f348d7b90df245bc56f5568c06d Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 21 Sep 2018 20:08:00 +0300 Subject: [PATCH 687/769] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 74118487..03a88d72 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -14,16 +14,16 @@ ## Types of changes -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [] Bug fix (non-breaking change which fixes an issue) +- [] New feature (non-breaking change which adds functionality) +- [] Breaking change (fix or feature that would cause existing functionality to not work as expected) ## Checklist: -- [ ] I have read the **CONTRIBUTING** document. -- [ ] My code follows the code style of this project. -- [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. -- [ ] I have added tests to cover my changes. -- [ ] All new and existing tests passed. +- [] I have read the **CONTRIBUTING** document. +- [] My code follows the code style of this project. +- [] My change requires a change to the documentation. +- [] I have updated the documentation accordingly. +- [] I have added tests to cover my changes. +- [] All new and existing tests passed. From 810358f1cc47eb11fbb506b7fd5d98b0904e4040 Mon Sep 17 00:00:00 2001 From: Gio d'Amelio Date: Fri, 21 Sep 2018 22:34:47 -0700 Subject: [PATCH 688/769] Update algo-showenv.sh to use `/usr/bin/env` in it's hashbang (#1126) Should allow better cross platform compatibility --- algo-showenv.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algo-showenv.sh b/algo-showenv.sh index 6085c407..41a6ff06 100755 --- a/algo-showenv.sh +++ b/algo-showenv.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Print information about Algo's invocation environment to aid in debugging. # This is normally called from Ansible right before a deployment gets underway. From 6c0753e3b89991e7ce0832bb297fd8d0eaf70c81 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 27 Sep 2018 11:18:00 +0300 Subject: [PATCH 689/769] GCE: Static external ip (optional) (#1125) --- config.cfg | 9 +++++---- roles/cloud-gce/tasks/main.yml | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/config.cfg b/config.cfg index 0967d260..fe6bbfd1 100644 --- a/config.cfg +++ b/config.cfg @@ -83,7 +83,7 @@ congrats: p12_pass: | "# The p12 and SSH keys password for new users is {{ p12_export_password }} #" ca_key_pass: | - "# The CA key password is {{ CA_password }} #" + "# The CA key password is {{ CA_password }} #" ssh_access: | "# Shell access: ssh -i {{ ansible_ssh_private_key_file|default(omit) }} {{ ansible_ssh_user|default(omit) }}@{{ ansible_ssh_host|default(omit) }} #" @@ -101,9 +101,9 @@ cloud_providers: image: "ubuntu-18-04-x64" # Change the encrypted flag to "true" to enable AWS volume encryption, for encryption of data at rest. # Warning: the Algo script will take approximately 6 minutes longer to complete. - # Also note that the documented AWS minimum permissions aren't sufficient. - # You will have to edit the AWS user policy documented at - # https://github.com/trailofbits/algo/blob/master/docs/cloud-amazon-ec2.md to also allow "ec2:CopyImage". + # Also note that the documented AWS minimum permissions aren't sufficient. + # You will have to edit the AWS user policy documented at + # https://github.com/trailofbits/algo/blob/master/docs/cloud-amazon-ec2.md to also allow "ec2:CopyImage". # See https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage-edit.html ec2: encrypted: false @@ -114,6 +114,7 @@ cloud_providers: gce: size: f1-micro image: ubuntu-1804 + external_static_ip: false lightsail: size: nano_1_0 image: ubuntu_16_04 diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 8dad0a08..8af6ff87 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -14,10 +14,27 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" + - block: + - name: External IP allocated + gce_eip: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + name: "{{ algo_server_name }}" + region: "{{ algo_region.split('-')[0:2] | join('-') }}" + state: present + register: gce_eip + + - name: Set External IP as a fact + set_fact: + external_ip: "{{ gce_eip.address }}" + when: cloud_providers.gce.external_static_ip + - name: "Creating a new instance..." gce: instance_names: "{{ algo_server_name }}" zone: "{{ algo_region }}" + external_ip: "{{ external_ip | default('ephemeral') }}" machine_type: "{{ cloud_providers.gce.size }}" image: "{{ cloud_providers.gce.image }}" service_account_email: "{{ service_account_email }}" From dbd68aa97d81e3286264344c112744554e2bd5b3 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 27 Sep 2018 11:18:12 +0300 Subject: [PATCH 690/769] WireGuard BSD (#1083) * WireGuard BSD * Remove unneeded config option * Enable PersistentKeepalive for NAT and Firewall Traversal Persistence * Install dnscrypt-proxy from repositories --- cloud.yml | 2 +- input.yml | 2 +- roles/common/tasks/facts.yml | 4 ++ .../dns_encryption/files/rc.dnscrypt-proxy.sh | 38 ------------- roles/dns_encryption/tasks/freebsd.yml | 55 +++---------------- .../templates/dnscrypt-proxy.toml.j2 | 2 +- roles/vpn/tasks/main.yml | 6 -- roles/wireguard/files/wireguard.sh | 40 ++++++++++++++ roles/wireguard/handlers/main.yml | 2 +- roles/wireguard/tasks/freebsd.yml | 16 ++++++ roles/wireguard/tasks/keys.yml | 6 +- roles/wireguard/tasks/main.yml | 42 ++++---------- roles/wireguard/tasks/ubuntu.yml | 32 +++++++++++ roles/wireguard/templates/client.conf.j2 | 1 + roles/wireguard/templates/server.conf.j2 | 1 - server.yml | 4 ++ 16 files changed, 123 insertions(+), 130 deletions(-) delete mode 100644 roles/dns_encryption/files/rc.dnscrypt-proxy.sh create mode 100644 roles/wireguard/files/wireguard.sh create mode 100644 roles/wireguard/tasks/freebsd.yml create mode 100644 roles/wireguard/tasks/ubuntu.yml diff --git a/cloud.yml b/cloud.yml index 3a4e299f..671c7765 100644 --- a/cloud.yml +++ b/cloud.yml @@ -1,7 +1,7 @@ --- - name: Provision the server hosts: localhost - tags: algo + tags: always vars_files: - config.cfg diff --git a/input.yml b/input.yml index aeb53192..18534518 100644 --- a/input.yml +++ b/input.yml @@ -1,7 +1,7 @@ --- - name: Ask user for the input hosts: localhost - tags: algo + tags: always vars: defaults: server_name: algo diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 8182cf20..29ee3f55 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -23,4 +23,8 @@ - set_fact: CA_password: "{{ CA_password.stdout }}" IP_subject_alt_name: "{{ IP_subject_alt_name }}" + +- name: Set IPv6 support as a fact + set_fact: ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" + tags: always diff --git a/roles/dns_encryption/files/rc.dnscrypt-proxy.sh b/roles/dns_encryption/files/rc.dnscrypt-proxy.sh deleted file mode 100644 index da35d896..00000000 --- a/roles/dns_encryption/files/rc.dnscrypt-proxy.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -# PROVIDE: dnscrypt-proxy -# REQUIRE: LOGIN -# BEFORE: securelevel -# KEYWORD: shutdown - -# Add the following lines to /etc/rc.conf to enable `dnscrypt-proxy': -# -# dnscrypt_proxy_enable="YES" -# dnscrypt_proxy_flags="" -# -# See rsync(1) for rsyncd_flags -# - -. /etc/rc.subr - -name="dnscrypt-proxy" -rcvar=dnscrypt_proxy_enable -load_rc_config "$name" -pidfile="/var/run/$name.pid" -start_cmd=dnscrypt_proxy_start -stop_postcmd=dnscrypt_proxy_stop - -: ${dnscrypt_proxy_enable="NO"} -: ${dnscrypt_proxy_flags="-config /usr/local/etc/dnscrypt-proxy/dnscrypt-proxy.toml"} - -dnscrypt_proxy_start() { - echo "Starting dnscrypt-proxy..." - touch ${pidfile} - /usr/sbin/daemon -cS -T dnscrypt-proxy -p ${pidfile} /usr/dnscrypt-proxy/freebsd-amd64/dnscrypt-proxy ${dnscrypt_proxy_flags} -} - -dnscrypt_proxy_stop() { - [ -f ${pidfile} ] && rm ${pidfile} -} - -run_rc_command "$1" diff --git a/roles/dns_encryption/tasks/freebsd.yml b/roles/dns_encryption/tasks/freebsd.yml index 30e0186c..bdada6fe 100644 --- a/roles/dns_encryption/tasks/freebsd.yml +++ b/roles/dns_encryption/tasks/freebsd.yml @@ -1,51 +1,10 @@ --- -- name: FreeBSD | Ensure that the required directories exist - file: - path: "{{ item }}" - state: directory - with_items: - - "{{ config_prefix|default('/') }}etc/dnscrypt-proxy/" - - /usr/dnscrypt-proxy/ - -- name: Required tools installed +- name: Install dnscrypt-proxy package: - name: gtar + name: dnscrypt-proxy2 -- name: FreeBSD | Retrive the latest versions - uri: - url: https://api.github.com/repos/jedisct1/dnscrypt-proxy/releases/latest - register: dnscrypt_proxy_latest - ignore_errors: true - -- name: FreeBSD | Set default dnscrypt-proxy assets - set_fact: - dnscrypt_proxy_latest: - json: - assets: - - name: "dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" - browser_download_url: "https://github.com/jedisct1/dnscrypt-proxy/releases/download/{{ dnscrypt_proxy_version }}/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" - when: dnscrypt_proxy_latest.failed - -- name: FreeBSD | Download the latest archive - get_url: - url: "{{ item['browser_download_url'] }}" - dest: "/tmp/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz" - mode: '0755' - force: true - with_items: "{{ dnscrypt_proxy_latest['json']['assets'] }}" - no_log: true - when: '"freebsd_amd64" in item.name and not item.name.endswith("minisig")' - notify: restart dnscrypt-proxy - -- name: FreeBSD | Extract the latest archive - unarchive: - remote_src: true - src: /tmp/dnscrypt-proxy-freebsd_amd64-{{ dnscrypt_proxy_version }}.tar.gz - dest: /usr/dnscrypt-proxy - -- name: FreeBSD | Configure rc script - copy: - src: rc.dnscrypt-proxy.sh - dest: /usr/local/etc/rc.d/dnscrypt-proxy - mode: "0755" - notify: restart dnscrypt-proxy +- name: Enable mac_portacl + lineinfile: + path: /etc/rc.conf + line: 'dnscrypt_proxy_mac_portacl_enable="YES"' + when: listen_port|int == 53 diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index 18a8bebb..aba1919e 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -151,7 +151,7 @@ tls_disable_session_tickets = true ## People in China may need to use 114.114.114.114:53 here. ## Other popular options include 8.8.8.8 and 1.1.1.1. -fallback_resolver = '127.0.0.53:53' +fallback_resolver = '{% if ansible_distribution == "FreeBSD" %}{{ ansible_dns.nameservers.0 }}:53{% else %}127.0.0.53:53{% endif %}' ## Never try to use the system DNS settings; unconditionally use the diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index de3a9f1d..2a7a90b2 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,11 +1,5 @@ --- - block: - - name: Include WireGuard role - include_role: - name: wireguard - tags: wireguard - when: wireguard_enabled and ansible_distribution == 'Ubuntu' - - name: Ensure that the strongswan group exist group: name=strongswan state=present diff --git a/roles/wireguard/files/wireguard.sh b/roles/wireguard/files/wireguard.sh new file mode 100644 index 00000000..efcde0e3 --- /dev/null +++ b/roles/wireguard/files/wireguard.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# PROVIDE: wireguard +# REQUIRE: LOGIN +# BEFORE: securelevel +# KEYWORD: shutdown + +. /etc/rc.subr + +name="wg" +rcvar=wg_enable + +command="/usr/local/bin/wg-quick" +start_cmd=wg_up +stop_cmd=wg_down +status_cmd=wg_status +pidfile="/var/run/$name.pid" +load_rc_config "$name" + +: ${wg_enable="NO"} +: ${wg_interface="wg0"} + +wg_up() { + echo "Starting WireGuard..." + /usr/sbin/daemon -cS -p ${pidfile} ${command} up ${wg_interface} +} + +wg_down() { + echo "Stopping WireGuard..." + ${command} down ${wg_interface} +} + +wg_status () { + not_running () { + echo "WireGuard is not running on $wg_interface" && exit 1 + } + /usr/local/bin/wg show wg0 && echo "WireGuard is running on $wg_interface" || not_running +} + +run_rc_command "$1" diff --git a/roles/wireguard/handlers/main.yml b/roles/wireguard/handlers/main.yml index 1063f5e6..d13ee31c 100644 --- a/roles/wireguard/handlers/main.yml +++ b/roles/wireguard/handlers/main.yml @@ -1,5 +1,5 @@ --- - name: restart wireguard service: - name: "wg-quick@{{ wireguard_interface }}" + name: "{{ service_name }}" state: restarted diff --git a/roles/wireguard/tasks/freebsd.yml b/roles/wireguard/tasks/freebsd.yml new file mode 100644 index 00000000..63e7b48c --- /dev/null +++ b/roles/wireguard/tasks/freebsd.yml @@ -0,0 +1,16 @@ +--- +- name: BSD | WireGuard installed + package: + name: wireguard + state: present + +- set_fact: + service_name: wireguard + tags: always + +- name: BSD | Configure rc script + copy: + src: wireguard.sh + dest: /usr/local/etc/rc.d/wireguard + mode: "0755" + notify: restart wireguard diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml index b38ab1fb..33434081 100644 --- a/roles/wireguard/tasks/keys.yml +++ b/roles/wireguard/tasks/keys.yml @@ -1,7 +1,7 @@ --- - name: Delete the lock files file: - dest: "/etc/wireguard/private_{{ item }}.lock" + dest: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" state: absent when: keys_clean_all|bool == True with_items: @@ -12,7 +12,7 @@ command: wg genkey register: wg_genkey args: - creates: "/etc/wireguard/private_{{ item }}.lock" + creates: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" with_items: - "{{ users }}" - "{{ IP_subject_alt_name }}" @@ -31,7 +31,7 @@ - name: Touch the lock file file: - dest: "/etc/wireguard/private_{{ item }}.lock" + dest: "{{ config_prefix|default('/') }}etc/wireguard/private_{{ item }}.lock" state: touch with_items: - "{{ users }}" diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 232d080c..3621754c 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -1,27 +1,4 @@ --- -- name: WireGuard repository configured - apt_repository: - repo: ppa:wireguard/wireguard - state: present - register: result - until: result is succeeded - retries: 10 - delay: 3 - -- name: WireGuard installed - apt: - name: wireguard - state: present - update_cache: true - -- name: Configure unattended-upgrades - copy: - src: 50-wireguard-unattended-upgrades - dest: /etc/apt/apt.conf.d/50-wireguard-unattended-upgrades - owner: root - group: root - mode: 0644 - - name: Ensure the required directories exist file: dest: "{{ wireguard_config_path }}/{{ item }}" @@ -33,6 +10,16 @@ delegate_to: localhost become: false +- name: Include tasks for Ubuntu + include_tasks: ubuntu.yml + when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + tags: always + +- name: Include tasks for FreeBSD + include_tasks: freebsd.yml + when: ansible_distribution == 'FreeBSD' + tags: always + - name: Generate keys import_tasks: keys.yml tags: update-users @@ -40,16 +27,11 @@ - name: WireGuard configured template: src: server.conf.j2 - dest: "/etc/wireguard/{{ wireguard_interface }}.conf" + dest: "{{ config_prefix|default('/') }}etc/wireguard/{{ wireguard_interface }}.conf" mode: "0600" notify: restart wireguard tags: update-users -- name: WireGuard reload-module-on-update - file: - dest: /etc/wireguard/.reload-module-on-update - state: touch - - name: WireGuard users config generated template: src: client.conf.j2 @@ -62,7 +44,7 @@ - name: WireGuard enabled and started service: - name: "wg-quick@{{ wireguard_interface }}" + name: "{{ service_name }}" state: started enabled: true diff --git a/roles/wireguard/tasks/ubuntu.yml b/roles/wireguard/tasks/ubuntu.yml new file mode 100644 index 00000000..c75b8a7b --- /dev/null +++ b/roles/wireguard/tasks/ubuntu.yml @@ -0,0 +1,32 @@ +--- +- name: WireGuard repository configured + apt_repository: + repo: ppa:wireguard/wireguard + state: present + register: result + until: result is succeeded + retries: 10 + delay: 3 + +- name: WireGuard installed + apt: + name: wireguard + state: present + update_cache: true + +- name: WireGuard reload-module-on-update + file: + dest: /etc/wireguard/.reload-module-on-update + state: touch + +- name: Configure unattended-upgrades + copy: + src: 50-wireguard-unattended-upgrades + dest: /etc/apt/apt.conf.d/50-wireguard-unattended-upgrades + owner: root + group: root + mode: 0644 + +- set_fact: + service_name: "wg-quick@{{ wireguard_interface }}" + tags: always diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index f75f0f43..6432e0ad 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -9,3 +9,4 @@ DNS = {{ wireguard_dns_servers }} PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} +PersistentKeepalive = 25 diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index d9468de4..adda0bed 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -5,7 +5,6 @@ Address = {{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['pre ListenPort = {{ wireguard_port }} PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} SaveConfig = false -Table = off {% for u in users %} diff --git a/server.yml b/server.yml index e7e4ad2a..4f8ad7cd 100644 --- a/server.yml +++ b/server.yml @@ -9,6 +9,7 @@ roles: - role: common + tags: common - role: dns_encryption when: dns_encryption tags: dns_encryption @@ -18,6 +19,9 @@ - role: ssh_tunneling when: algo_ssh_tunneling tags: ssh_tunneling + - role: wireguard + when: wireguard_enabled + tags: wireguard - role: vpn tags: vpn From 144258668271b7088f9242793f569b639a55d882 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 30 Sep 2018 05:25:02 +0300 Subject: [PATCH 691/769] WireGuard: Generate QR codes (#1129) * WireGuard: Generate QR codes * Update client-android.md --- docs/client-android.md | 3 +-- requirements.txt | 1 + roles/wireguard/tasks/main.yml | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/client-android.md b/docs/client-android.md index 1e98f6d7..553b5071 100644 --- a/docs/client-android.md +++ b/docs/client-android.md @@ -3,5 +3,4 @@ ## Installation via profiles 1. [Install the WireGuard VPN Client](https://play.google.com/store/apps/details?id=com.wireguard.android). -2. Copy `wireguard/{username}.conf` to your phone's internal storage. -3. Open the WireGuard app and add a connection using your AlgoVPN configuration file. +2. Open QR code `configs//wireguard/.png` and scan it in the WireGuard app diff --git a/requirements.txt b/requirements.txt index f2580658..4d40c39b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pyopenssl jinja2==2.8 shade pycrypto +segno diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 3621754c..dacedb56 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -42,6 +42,20 @@ delegate_to: localhost become: false +- name: Generate QR codes + shell: > + umask 077; + which segno && + segno --scale=5 --output={{ item.1 }}.png \ + "{{ lookup('template', 'client.conf.j2') }}" || true + changed_when: false + with_indexed_items: "{{ users }}" + delegate_to: localhost + become: false + args: + chdir: "{{ wireguard_config_path }}" + executable: bash + - name: WireGuard enabled and started service: name: "{{ service_name }}" From d7dcaeb575e9f0c5bd852b8fe2e378e7bdff4db2 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 4 Oct 2018 14:36:54 +0300 Subject: [PATCH 692/769] Update troubleshooting.md Fixes #1118 --- docs/troubleshooting.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 53bacb11..90c14a5e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,7 @@ # Troubleshooting +First of all, check [this](https://github.com/trailofbits/algo#features) and ensure that you are deploying to the supported ubuntu version. + * [Installation Problems](#installation-problems) * [Error: "You have not agreed to the Xcode license agreements"](#error-you-have-not-agreed-to-the-xcode-license-agreements) * [Error: checking whether the C compiler works... no](#error-checking-whether-the-c-compiler-works-no) From d90ba3d11a18bd0edfd5fc2c678420e234f55330 Mon Sep 17 00:00:00 2001 From: David Myers Date: Thu, 4 Oct 2018 18:12:48 -0400 Subject: [PATCH 693/769] Allow more flexible DNSCrypt configuration (#1120) * Allow more flexible DNSCrypt configuration * Correct permissions on files changed in #1120 I'm not sure why using BBEdit over SMB makes every file executable. * Put the public resolvers cache file in /tmp. --- config.cfg | 20 +++++++++++++++---- roles/dns_encryption/defaults/main.yml | 6 +++++- .../templates/dnscrypt-proxy.toml.j2 | 8 ++++++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/config.cfg b/config.cfg index fe6bbfd1..f1721e5e 100644 --- a/config.cfg +++ b/config.cfg @@ -38,13 +38,25 @@ adblock_lists: - "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "https://hosts-file.net/ad_servers.txt" -# Enable DNS encryption. Use dns_encryption_provider to specify the provider. If false dns_servers should be specified +# Enable DNS encryption. +# If 'false', 'dns_servers' should be specified below. dns_encryption: true -# Possible values: google, cloudflare -dns_encryption_provider: cloudflare +# DNS servers which will be used if 'dns_encryption' is 'true'. Multiple +# providers may be specified, but avoid mixing providers that filter results +# (like Cisco) with those that don't (like Cloudflare) or you could get +# inconsistent results. The list of available public providers can be found +# here: +# https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v2/public-resolvers.md +dnscrypt_servers: + ipv4: + - cloudflare +# - google + ipv6: + - cloudflare-ipv6 -# DNS servers which will be used if dns_encryption disabled +# DNS servers which will be used if 'dns_encryption' is 'false'. +# The default is to use Cloudflare. dns_servers: ipv4: - 1.1.1.1 diff --git a/roles/dns_encryption/defaults/main.yml b/roles/dns_encryption/defaults/main.yml index 5997f58a..1869e6a2 100644 --- a/roles/dns_encryption/defaults/main.yml +++ b/roles/dns_encryption/defaults/main.yml @@ -5,5 +5,9 @@ listen_port: "{% if algo_local_dns %}5353{% else %}53{% endif %}" dnscrypt_proxy_version: 2.0.10 apparmor_enabled: true dns_encryption: true -dns_encryption_provider: "*" ipv6_support: false +dnscrypt_servers: + ipv4: + - cloudflare + ipv6: + - cloudflare-ipv6 diff --git a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 index aba1919e..d954ff8b 100644 --- a/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 +++ b/roles/dns_encryption/templates/dnscrypt-proxy.toml.j2 @@ -27,7 +27,11 @@ ## The proxy will automatically pick the fastest, working servers from the list. ## Remove the leading # first to enable this; lines starting with # are ignored. -server_names = ['{{ dns_encryption_provider }}'{% if ipv6_support and dns_encryption_provider == "cloudflare" %}, '{{ dns_encryption_provider }}-ipv6' {% endif %} ] +{# Allow either list to be empty. Output nothing if both are empty. #} +{% set servers = [] %} +{% if dnscrypt_servers.ipv4 %}{% set servers = dnscrypt_servers.ipv4 %}{% endif %} +{% if ipv6_support and dnscrypt_servers.ipv6 %}{% set servers = servers + dnscrypt_servers.ipv6 %}{% endif %} +{% if servers %}server_names = ['{{ servers | join("', '") }}']{% endif %} ## List of local addresses and ports to listen to. Can be IPv4 and/or IPv6. @@ -446,7 +450,7 @@ cache_neg_max_ttl = 600 [sources.'public-resolvers'] urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v2/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md'] - cache_file = 'public-resolvers.md' + cache_file = '/tmp/public-resolvers.md' minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3' refresh_delay = 72 prefix = '' From cd3fbe5e47f8798f8a3acac675342bbf4acc2f6b Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 5 Oct 2018 10:29:09 -0400 Subject: [PATCH 694/769] Add WireGuard port to FAQ (#1141) --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 00b44f83..db11965d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -74,4 +74,4 @@ No. ## What inbound ports are used? -You should only need 22/TCP, 500/UDP, and 4500/UDP. +You should only need 22/TCP, 500/UDP, 4500/UDP, and 51820/UDP opened on any firewall that sits between your clients and your Algo server. From bcba9055474ea99ead92786729266f1b3d186e19 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 8 Oct 2018 03:33:55 +0300 Subject: [PATCH 695/769] ssh tunneling fixes (#1127) --- roles/ssh_tunneling/tasks/main.yml | 41 +++++++++----------------- roles/vpn/defaults/main.yml | 1 + roles/vpn/tasks/main.yml | 10 +++---- roles/vpn/tasks/openssl.yml | 27 +++++++++++++++-- roles/vpn/templates/strongswan.conf.j2 | 2 +- server.yml | 6 ++-- users.yml | 4 +-- 7 files changed, 50 insertions(+), 41 deletions(-) diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 860a329d..259464b4 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -31,25 +31,20 @@ groups: algo home: '/var/jail/{{ item }}' createhome: yes - generate_ssh_key: yes + generate_ssh_key: false shell: /bin/false - ssh_key_type: ecdsa - ssh_key_bits: 256 - ssh_key_comment: '{{ item }}@{{ IP_subject_alt_name }}' - ssh_key_passphrase: "{{ p12_export_password }}" - update_password: on_create state: present append: yes with_items: "{{ users }}" tags: update-users - name: The authorized keys file created - file: - src: '/var/jail/{{ item }}/.ssh/id_ecdsa.pub' - dest: '/var/jail/{{ item }}/.ssh/authorized_keys' - owner: "{{ item }}" - group: "{{ item }}" - state: link + authorized_key: + user: "{{ item }}" + key: "{{ lookup('file', 'configs/' + IP_subject_alt_name + '/pki/public/' + item + '.pub') }}" + state: present + manage_dir: true + exclusive: true with_items: "{{ users }}" tags: update-users @@ -57,15 +52,6 @@ shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null register: ssh_fingerprints - - name: Fetch users SSH private keys - fetch: - src: '/var/jail/{{ item }}/.ssh/id_ecdsa' - dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem - flat: yes - mode: "0600" - with_items: "{{ users }}" - tags: update-users - - name: Fetch the known_hosts file local_action: module: template @@ -83,20 +69,21 @@ tags: update-users with_items: "{{ users }}" - - name: SSH | Get active system users - shell: > - getent group algo | cut -f4 -d: | sed "s/,/\n/g" - register: valid_users + - name: Get active users + getent: + database: group + key: algo + split: ':' tags: update-users - - name: SSH | Delete non-existing users + - name: Delete non-existing users user: name: "{{ item }}" state: absent remove: yes force: yes when: item not in users - with_items: "{{ valid_users.stdout_lines | default('null') }}" + with_items: "{{ getent_group['algo'][2].split(',') }}" tags: update-users rescue: - debug: var=fail_hint diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 51b06bf8..a7e3ea0f 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -34,6 +34,7 @@ ipv6_support: false dns_encryption: true domain: false subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" +subjectAltName_USER: "{% if '@' in item %}email:{{ item }}{% else %}DNS:{{ item }}{% endif %}" openssl_bin: openssl strongswan_enabled_plugins: - aes diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 2a7a90b2..27be701a 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -1,10 +1,10 @@ --- - block: - - name: Ensure that the strongswan group exist - group: name=strongswan state=present - - - name: Ensure that the strongswan user exist - user: name=strongswan group=strongswan state=present + - name: Include WireGuard role + include_role: + name: wireguard + tags: wireguard + when: wireguard_enabled and ansible_distribution == 'Ubuntu' - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index acd966c6..a8175977 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -16,12 +16,14 @@ dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" state: directory recurse: yes + mode: '0700' with_items: - ecparams - certs - crl - newcerts - private + - public - reqs - name: Ensure the files exist @@ -42,6 +44,7 @@ - name: Build the CA pair shell: > + umask 077; {{ openssl_bin }} ecparam -name secp384r1 -out ecparams/secp384r1.pem && {{ openssl_bin }} req -utf8 -new -newkey ec:ecparams/secp384r1.pem @@ -70,6 +73,7 @@ - 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 }}")) @@ -92,9 +96,10 @@ - name: Build the client's pair shell: > + umask 077; {{ openssl_bin }} req -utf8 -new -newkey ec:ecparams/secp384r1.pem - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) -keyout private/{{ item }}.key -out reqs/{{ item }}.req -nodes -passin pass:"{{ CA_password }}" @@ -102,7 +107,7 @@ {{ openssl_bin }} ca -utf8 -in reqs/{{ item }}.req -out certs/{{ item }}.crt - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) -days 3650 -batch -passin pass:"{{ CA_password }}" -subj "/CN={{ item }}" && @@ -113,8 +118,24 @@ executable: bash with_items: "{{ users }}" + - name: Create links for the private keys + file: + src: "pki/private/{{ item }}.key" + dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem" + state: link + force: true + with_items: "{{ users }}" + + - name: Build openssh public keys + openssl_publickey: + path: "configs/{{ IP_subject_alt_name }}/pki/public/{{ item }}.pub" + privatekey_path: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.key" + format: OpenSSH + with_items: "{{ users }}" + - name: Build the client's p12 shell: > + umask 077; {{ openssl_bin }} pkcs12 -in certs/{{ item }}.crt -inkey private/{{ item }}.key @@ -149,7 +170,7 @@ - name: Revoke non-existing users shell: > {{ openssl_bin }} ca -gencrl - -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName=DNS:{{ item }}")) + -config <(cat openssl.cnf <(printf "[basic_exts]\nsubjectAltName={{ subjectAltName_USER }}")) -passin pass:"{{ CA_password }}" -revoke certs/{{ item }}.crt -out crl/{{ item }}.crt diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index b658ac08..7fcf9ef4 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -10,7 +10,7 @@ charon { include strongswan.d/charon/*.conf } user = strongswan - group = strongswan + group = nogroup {% if ansible_distribution == 'FreeBSD' %} filelog { /var/log/charon.log { diff --git a/server.yml b/server.yml index 4f8ad7cd..b6e8340b 100644 --- a/server.yml +++ b/server.yml @@ -16,14 +16,14 @@ - role: dns_adblocking when: algo_local_dns tags: dns_adblocking - - role: ssh_tunneling - when: algo_ssh_tunneling - tags: ssh_tunneling - role: wireguard when: wireguard_enabled tags: wireguard - role: vpn tags: vpn + - role: ssh_tunneling + when: algo_ssh_tunneling + tags: ssh_tunneling post_tasks: - block: diff --git a/users.yml b/users.yml index 36f162f5..bb934946 100644 --- a/users.yml +++ b/users.yml @@ -60,13 +60,13 @@ roles: - role: common - - role: ssh_tunneling - when: algo_ssh_tunneling - role: wireguard tags: [ 'vpn', 'wireguard' ] when: wireguard_enabled - role: vpn tags: vpn + - role: ssh_tunneling + when: algo_ssh_tunneling post_tasks: - block: From efc8dc7620bc8692c8bd27f3b79fa8cd18d1bdaf Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 14 Oct 2018 10:22:45 +0300 Subject: [PATCH 696/769] add tags for the wireguard qr code task. variables fix (#1147) --- roles/vpn/tasks/openssl.yml | 2 ++ roles/wireguard/tasks/main.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/roles/vpn/tasks/openssl.yml b/roles/vpn/tasks/openssl.yml index a8175977..3a286be7 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/vpn/tasks/openssl.yml @@ -196,6 +196,8 @@ executable: bash delegate_to: localhost become: no + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" - name: Copy the CRL to the vpn server copy: diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index dacedb56..f52183d0 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -52,6 +52,9 @@ with_indexed_items: "{{ users }}" delegate_to: localhost become: false + tags: update-users + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" args: chdir: "{{ wireguard_config_path }}" executable: bash From fbc7b29456dcc13e7d7e1c5323f72b9652ad3abb Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 22 Oct 2018 23:49:09 +0300 Subject: [PATCH 697/769] WireGuard update-users fix (#1154) --- roles/wireguard/defaults/main.yml | 3 +++ roles/wireguard/tasks/main.yml | 13 ++++++++++++- roles/wireguard/templates/client.conf.j2 | 4 +--- roles/wireguard/templates/server.conf.j2 | 9 +++------ 4 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 roles/wireguard/defaults/main.yml diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml new file mode 100644 index 00000000..51ef2279 --- /dev/null +++ b/roles/wireguard/defaults/main.yml @@ -0,0 +1,3 @@ +--- +wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + item.0 + 1 }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + item.0 + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index f52183d0..369f88c7 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -7,6 +7,7 @@ with_items: - private - public + - ip delegate_to: localhost become: false @@ -24,6 +25,16 @@ import_tasks: keys.yml tags: update-users +- name: Dump IP addresses + copy: + dest: "{{ wireguard_config_path }}/ip/{{ item.1 }}" + content: "{{ wireguard_client_ip }}" + force: false + with_indexed_items: "{{ users }}" + tags: update-users + become: false + delegate_to: localhost + - name: WireGuard configured template: src: server.conf.j2 @@ -38,9 +49,9 @@ dest: "{{ wireguard_config_path }}/{{ item.1 }}.conf" mode: "0600" with_indexed_items: "{{ users }}" + become: false tags: update-users delegate_to: localhost - become: false - name: Generate QR codes shell: > diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 6432e0ad..d7645be7 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -1,8 +1,6 @@ [Interface] PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} -Address = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + item.0 + 1 }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + item.0 + 1 }}/{{ wireguard_network_ipv6['prefix'] }} -{% endif %} - +Address = {{ lookup('file', wireguard_config_path + '/ip/' + item.1) }} DNS = {{ wireguard_dns_servers }} [Peer] diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index adda0bed..a2307d87 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -1,16 +1,13 @@ [Interface] -Address = {{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }} -{% endif %} - +Address = {{ wireguard_server_ip }} ListenPort = {{ wireguard_port }} PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} SaveConfig = false -{% for u in users %} +{% for u in users|sort %} [Peer] # {{ u }} PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} -AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + loop.index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + loop.index }}/128 -{% endif %} +AllowedIPs = {{ lookup('file', wireguard_config_path + '/ip/' + u) }} {% endfor %} From 3468d27e615f8532f550518043ce4539ebd9d09e Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 22 Oct 2018 23:49:18 +0300 Subject: [PATCH 698/769] Lightsail back (#1157) --- CHANGELOG.md | 4 ++++ README.md | 2 +- config.cfg | 2 +- input.yml | 1 + roles/cloud-lightsail/tasks/prompts.yml | 4 ++-- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 417b757d..27bd579d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 20 Oct 2018 +### Added +- AWS Lightsail + ## 7 Sep 2018 ### Changed - Azure: Deployment via Azure Resource Manager diff --git a/README.md b/README.md index 26440fd3..ccd21c4d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server ## Anti-features diff --git a/config.cfg b/config.cfg index f1721e5e..03f439e9 100644 --- a/config.cfg +++ b/config.cfg @@ -129,7 +129,7 @@ cloud_providers: external_static_ip: false lightsail: size: nano_1_0 - image: ubuntu_16_04 + image: ubuntu_18_04 scaleway: size: START1-S image: Ubuntu Bionic Beaver diff --git a/input.yml b/input.yml index 18534518..f24ab2ba 100644 --- a/input.yml +++ b/input.yml @@ -13,6 +13,7 @@ store_cakey: false providers_map: - { name: DigitalOcean, alias: digitalocean } + - { name: Amazon Lightsail, alias: lightsail } - { name: Amazon EC2, alias: ec2 } - { name: Vultr, alias: vultr } - { name: Microsoft Azure, alias: azure } diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml index 26d50a57..de6a02d9 100644 --- a/roles/cloud-lightsail/tasks/prompts.yml +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -37,7 +37,7 @@ set_fact: default_region: >- {% for r in lightsail_regions %} - {%- if r['name'] == "eu-west-1" %}{{ loop.index }}{% endif %} + {%- if r['name'] == "us-east-1" %}{{ loop.index }}{% endif %} {%- endfor %} - pause: @@ -45,7 +45,7 @@ What region should the server be located in? (https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/) {% for r in lightsail_regions %} - {{ loop.index }}. {{ r['name'] }} {{ r['displayName'] }} + {{ (loop.index|string + '.').ljust(3) }} {{ r['name'].ljust(20) }} {{ r['displayName'] }} {% endfor %} Enter the number of your desired region From 54a91447bf9e873c7bbd7066d5f5ba8dae2d6064 Mon Sep 17 00:00:00 2001 From: Bruno Tavares Date: Sun, 28 Oct 2018 03:35:43 -0300 Subject: [PATCH 699/769] Add documentation on how to setup GCE accounts (#1164) * Add documentation on how to setup GCE accounts This commit adds the steps needed to create a credential with the needed access on Google Cloud Platform to be able to successfully create a new algo VPN. Related to: - https://github.com/trailofbits/algo/issues/682 - https://github.com/trailofbits/algo/issues/658 * Adds links on main README to GCP * Adds link to Ansible documentation * Update cloud-gce.md --- README.md | 1 + docs/cloud-gce.md | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 docs/cloud-gce.md diff --git a/README.md b/README.md index ccd21c4d..68b2648d 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ After this process completes, the Algo VPN server will contains only the users l - Configure [Amazon EC2](docs/cloud-amazon-ec2.md) - Configure [Azure](docs/cloud-azure.md) - Configure [DigitalOcean](docs/cloud-do.md) + - Configure [Google Cloud Platform](docs/cloud-gce.md) * Advanced Deployment - Deploy to your own [FreeBSD](docs/deploy-to-freebsd.md) server - Deploy to your own [Ubuntu 18.04](docs/deploy-to-ubuntu.md) server diff --git a/docs/cloud-gce.md b/docs/cloud-gce.md new file mode 100644 index 00000000..fe43c43a --- /dev/null +++ b/docs/cloud-gce.md @@ -0,0 +1,41 @@ +# Google Cloud Platform setup + +Follow the [installation instructions](https://cloud.google.com/sdk/) to have the CLI commands to interact with Google. + +After creating an account and installing, login in on your account using `gcloud init` + +### Creating a project + +The recommendation on GCP is to group resources on **Projets**, so we will create one project to put our VPN server and service account restricted to it. + +```bash +## Create the project to group the resources +### You might need to change it to have a global unique project id +PROJECT_ID=${USER}-algo-vpn +BILLING_ID="$(gcloud beta billing accounts list --format="value(ACCOUNT_ID)")" + +gcloud projects create ${PROJECT_ID} --name algo-vpn --set-as-default +gcloud beta billing projects link ${PROJECT_ID} --billing-account ${BILLING_ID} + +## Create an account that have access to the VPN +gcloud iam service-accounts create algo-vpn --display-name "Algo VPN" +gcloud iam service-accounts keys create configs/gce.json \ + --iam-account algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member serviceAccount:algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/compute.admin +gcloud projects add-iam-policy-binding ${PROJECT_ID} \ + --member serviceAccount:algo-vpn@${PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/iam.serviceAccountUser + +## Enable the services +gcloud services enable compute.googleapis.com + +./algo -e "provider=gce" -e "gce_credentials_file=$(pwd)/configs/gce.json" + +``` + +**Attention:** take care of the `configs/gce.json` file, which contains the credentials to manage your Google Cloud account, including create and delete servers on this project. + + +There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md) From 465cbeb7e09fbb5acece73d2e0b7265b8134a536 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Tue, 30 Oct 2018 07:59:50 +0100 Subject: [PATCH 700/769] Update StrongSwan setup docs (#1181) --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 68b2648d..282de737 100644 --- a/README.md +++ b/README.md @@ -132,11 +132,13 @@ One common use case is to let your server access your local LAN without going th conn lan-passthrough leftsubnet=192.168.1.1/24 # Replace with your LAN subnet - rightsubnet=192.168.1.1/24 # Replac with your LAND subnet + rightsubnet=192.168.1.1/24 # Replace with your LAN subnet authby=never # No authentication necessary type=pass # passthrough auto=route # no need to ipsec up lan-passthrough +To configure the connection to come up at boot time replace `auto=add` with `auto=start`. + ### Other Devices Depending on the platform, you may need one or multiple of the following files. From 399d47233a8548e9576cb7e2726de52d6a1c1164 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 1 Nov 2018 20:59:14 +0100 Subject: [PATCH 701/769] add region (#1182) --- roles/cloud-lightsail/tasks/prompts.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml index de6a02d9..ff3d23e3 100644 --- a/roles/cloud-lightsail/tasks/prompts.yml +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -27,6 +27,7 @@ lightsail_region_facts: aws_access_key: "{{ access_key }}" aws_secret_key: "{{ secret_key }}" + region: us-east-1 register: _lightsail_regions - name: Set facts about thre regions From 30446d03632327e345823ebaa2a101990b6f7f03 Mon Sep 17 00:00:00 2001 From: datew0 <44378542+datew0@users.noreply.github.com> Date: Fri, 2 Nov 2018 14:38:54 +0300 Subject: [PATCH 702/769] Set disk size depending on server plan (#1159) Scaleway`s START1-XS does not start with a disk size of 50GB. --- roles/cloud-scaleway/tasks/image_facts.yml | 1 + roles/cloud-scaleway/tasks/main.yml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/roles/cloud-scaleway/tasks/image_facts.yml b/roles/cloud-scaleway/tasks/image_facts.yml index 1faa3d33..41269845 100644 --- a/roles/cloud-scaleway/tasks/image_facts.yml +++ b/roles/cloud-scaleway/tasks/image_facts.yml @@ -6,4 +6,5 @@ when: - cloud_providers.scaleway.image == item.name - cloud_providers.scaleway.arch == item.arch + - server_disk_size == item.root_volume.size with_items: "{{ outer_item['json']['images'] }}" diff --git a/roles/cloud-scaleway/tasks/main.yml b/roles/cloud-scaleway/tasks/main.yml index ecf52e95..87ec1d7f 100644 --- a/roles/cloud-scaleway/tasks/main.yml +++ b/roles/cloud-scaleway/tasks/main.yml @@ -2,6 +2,15 @@ - name: Include prompts import_tasks: prompts.yml + - name: Set disk size + set_fact: + server_disk_size: 50000000000 + + - name: Check server size + set_fact: + server_disk_size: 25000000000 + when: cloud_providers.scaleway.size == "START1-XS" + - name: Check if server exists uri: url: "https://cp-{{ algo_region }}.scaleway.com/servers" From 2b2d90a8a9d265baf67020c88163ca29f6c8acea Mon Sep 17 00:00:00 2001 From: zuccs Date: Tue, 6 Nov 2018 02:35:01 +1100 Subject: [PATCH 703/769] Fix typo (#1165) --- roles/cloud-lightsail/tasks/prompts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-lightsail/tasks/prompts.yml b/roles/cloud-lightsail/tasks/prompts.yml index ff3d23e3..1c98c5ac 100644 --- a/roles/cloud-lightsail/tasks/prompts.yml +++ b/roles/cloud-lightsail/tasks/prompts.yml @@ -30,7 +30,7 @@ region: us-east-1 register: _lightsail_regions - - name: Set facts about thre regions + - name: Set facts about the regions set_fact: lightsail_regions: "{{ _lightsail_regions.results.regions | sort(attribute='name') }}" From a53dec6349d99111db8348664f8765b831b9bda7 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 6 Nov 2018 07:03:44 +0100 Subject: [PATCH 704/769] Closes #1189 --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 90c14a5e..6b1fc09e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -349,7 +349,7 @@ python2.7 -m virtualenv --python=`which python2.7` env && source env/bin/activat The problem may happen if you recently moved to a new server, where you have Algo VPN. 1. Clear the Networking caches: - - Run CDM (click windows start menu, type 'cmd', right click on 'Command Prompt' and select "Run as Administrator"). + - Run CMD (click windows start menu, type 'cmd', right click on 'Command Prompt' and select "Run as Administrator"). - Type the commands below: ``` netsh int ip reset From a76642c4d5c29f12efa9183cad465f1704a45a2d Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 12 Nov 2018 04:21:54 -0500 Subject: [PATCH 705/769] Update mobileconfig.j2 (#1197) Adds "Algo VPN" to the organization in the "Profiles" menu of "General Settings". (The type still shows up as "Unknown" in the "VPN" menu, because that seems to be governed by the "VPNSubType" string, which must be empty according to the [developer reference](https://developer.apple.com/enterprise/documentation/Configuration-Profile-Reference.pdf) Maybe this can help clear the way for #1101. --- roles/vpn/templates/mobileconfig.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 44fbcbda..54614fd4 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -178,6 +178,8 @@ {{ IP_subject_alt_name }} IKEv2 PayloadIdentifier donut.local.{{ 500000 | random | to_uuid | upper }} + PayloadOrganization + Algo VPN PayloadRemovalDisallowed PayloadType From 75685e202b884c2d873cdc35ea1ebfbecdf78af9 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 12 Nov 2018 08:01:37 -0500 Subject: [PATCH 706/769] Troubleshooting.md updates (#1195) * Troubleshooting.md updates Adds solutions to #1067 to the troubleshooting faq. Also moves a couple of answers to correspond to the headers. * Change to Algo, strongly rec Ubuntu 18.04 --- docs/troubleshooting.md | 62 ++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6b1fc09e..6e910218 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -17,6 +17,7 @@ First of all, check [this](https://github.com/trailofbits/algo#features) and ens * [DigitalOcean: error tagging resource 'xxxxxxxx': param is missing or the value is empty: resources](#digitalocean-error-tagging-resource) * [Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid](#windows-the-value-of-parameter-linuxconfigurationsshpublickeyskeydata-is-invalid) * [Docker: Failed to connect to the host via ssh](#docker-failed-to-connect-to-the-host-via-ssh) + * [Wireguard: Unable to find 'configs/...' in expected paths](#wireguard-unable-to-find-configs-in-expected-paths) * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -123,6 +124,22 @@ You tried to install Algo and you see an error that reads "ansible-playbook: com You did not finish step 4 in the installation instructions, "[Install Algo's remaining dependencies](https://github.com/trailofbits/algo#deploy-the-algo-server)." Algo depends on [Ansible](https://github.com/ansible/ansible), an automation framework, and this error indicates that you do not have Ansible installed. Ansible is installed by `pip` when you run `python -m pip install -r requirements.txt`. You must complete the installation instructions to run the Algo server deployment process. +### Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION + +You tried to install Algo and you received an error like this one: + +``` +Could not fetch URL https://pypi.python.org/simple/secretstorage/: There was a problem confirming the ssl certificate: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:590) - skipping + Could not find a version that satisfies the requirement SecretStorage<3 (from -r requirements.txt (line 2)) (from versions: ) +No matching distribution found for SecretStorage<3 (from -r requirements.txt (line 2)) +``` + +It's time to upgrade your python. + +`brew upgrade python2` + +You can also download python 2.7.x from python.org. + ### Bad owner or permissions on .ssh You tried to run Algo and it quickly exits with an error about a bad owner or permissions: @@ -171,6 +188,17 @@ Algo builds a [Cloudformation](https://aws.amazon.com/cloudformation/) template In many cases, failed deployments are the result of [service limits](http://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) being reached, such as "CREATE_FAILED AWS::EC2::VPC VPC The maximum number of VPCs has been reached." In these cases, you must either [delete the VPCs from previous deployments](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/working-with-vpcs.html#VPC_Deleting), or [contact AWS support](https://console.aws.amazon.com/support/home?region=us-east-1#/case/create?issueType=service-limit-increase&limitType=service-code-direct-connect) to increase the limits on your account. +### AWS: not authorized to perform: cloudformation:UpdateStack + +You tried to deploy Algo to AWS and you received an error like this one: + +``` +TASK [cloud-ec2 : Deploy the template] ***************************************** +fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "User: arn:aws:iam::082851645362:user/algo is not authorized to perform: cloudformation:UpdateStack on resource: arn:aws:cloudformation:us-east-1:082851645362:stack/algo/*"} +``` + +This error indicates you already have Algo deployed to Cloudformation. Need to [delete it](cloud-amazon-ec2.md#cleanup) first, then re-deploy. + ### DigitalOcean: error tagging resource You tried to deploy Algo to DigitalOcean and you received an error like this one: @@ -205,22 +233,6 @@ Target: linuxConfiguration.ssh.publicKeys.keyData"} This is related to [the chmod issue](https://github.com/Microsoft/WSL/issues/81) inside /mnt directory which is NTFS. The fix is to place Algo outside of /mnt directory. -### Could not fetch URL ... TLSV1_ALERT_PROTOCOL_VERSION - -You tried to install Algo and you received an error like this one: - -``` -Could not fetch URL https://pypi.python.org/simple/secretstorage/: There was a problem confirming the ssl certificate: [SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:590) - skipping - Could not find a version that satisfies the requirement SecretStorage<3 (from -r requirements.txt (line 2)) (from versions: ) -No matching distribution found for SecretStorage<3 (from -r requirements.txt (line 2)) -``` - -It's time to upgrade your python - -`brew upgrade python2` - -You can also download python 2.7.x from python.org - ### Docker: Failed to connect to the host via ssh You tried to deploy Algo from Docker and you received an error like this one: @@ -239,16 +251,22 @@ You need to add the following to the ansible.cfg in repo root: control_path_dir=/dev/shm/ansible_control_path ``` -### AWS: not authorized to perform: cloudformation:UpdateStack +### Wireguard: Unable to find 'configs/...' in expected paths -You tried to deploy Algo to AWS and you received an error like this one: +You tried to run Algo and you received an error like this one: ``` -TASK [cloud-ec2 : Deploy the template] ***************************************** -fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "User: arn:aws:iam::082851645362:user/algo is not authorized to perform: cloudformation:UpdateStack on resource: arn:aws:cloudformation:us-east-1:082851645362:stack/algo/*"} -``` +TASK [wireguard : Generate public keys] ******************************************************************************** +[WARNING]: Unable to find 'configs/xxx.xxx.xxx.xxx/wireguard//private/dan' in expected paths. -This error indicates you already have Algo deployed to Cloudformation. Need to [delete it](cloud-amazon-ec2.md#cleanup) first, then re-deploy. +fatal: [localhost]: FAILED! => {"msg": "An unhandled exception occurred while running the lookup plugin 'file'. Error was a , original message: could not locate file in lookup: configs/xxx.xxx.xxx.xxx/wireguard//private/dan"} +``` +This error is usually hit when using the local install option on a server that isn't Ubuntu 18.04. You should upgrade your server to Ubuntu 18.04. If this doesn't work, try removing `*.lock` files at /etc/wireguard/ as follows: + +```ssh +sudo rm -rf /etc/wireguard/*.lock +``` +Then immediately re-run `./algo`. ## Connection Problems From 66d30e3005b6f92079c0b8db15edeceef29ce0ec Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 12 Nov 2018 18:03:31 +0100 Subject: [PATCH 707/769] WireGuard update-users fix (#1183) --- roles/vpn/defaults/main.yml | 4 +- roles/wireguard/defaults/main.yml | 2 +- roles/wireguard/tasks/main.yml | 90 +++++++++++++----------- roles/wireguard/templates/client.conf.j2 | 2 +- roles/wireguard/templates/server.conf.j2 | 8 ++- 5 files changed, 58 insertions(+), 48 deletions(-) diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index a7e3ea0f..8e044f29 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -7,13 +7,13 @@ wireguard_network_ipv4: prefix: 24 gateway: 10.19.49.1 clients_range: 10.19.49 - clients_start: 100 + clients_start: 2 wireguard_network_ipv6: subnet: 'fd9d:bc11:4021::' prefix: 48 gateway: 'fd9d:bc11:4021::1' clients_range: 'fd9d:bc11:4021::' - clients_start: 100 + clients_start: 2 wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" keys_clean_all: false diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 51ef2279..90da64f5 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -1,3 +1,3 @@ --- -wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + item.0 + 1 }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + item.0 + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index 369f88c7..fa184fdc 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -7,7 +7,6 @@ with_items: - private - public - - ip delegate_to: localhost become: false @@ -25,50 +24,57 @@ import_tasks: keys.yml tags: update-users -- name: Dump IP addresses - copy: - dest: "{{ wireguard_config_path }}/ip/{{ item.1 }}" - content: "{{ wireguard_client_ip }}" - force: false - with_indexed_items: "{{ users }}" - tags: update-users - become: false - delegate_to: localhost +- block: + - block: + - name: WireGuard user list updated + lineinfile: + dest: "{{ wireguard_config_path }}/index.txt" + create: true + mode: "0600" + insertafter: EOF + line: "{{ item }}" + register: lineinfile + with_items: "{{ users }}" -- name: WireGuard configured - template: - src: server.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/wireguard/{{ wireguard_interface }}.conf" - mode: "0600" - notify: restart wireguard + - set_fact: + wireguard_users: "{{ (lookup('file', wireguard_config_path + 'index.txt')).split('\n') }}" + + - name: WireGuard users config generated + template: + src: client.conf.j2 + dest: "{{ wireguard_config_path }}/{{ item.1 }}.conf" + mode: "0600" + with_indexed_items: "{{ wireguard_users }}" + when: item.1 in users + vars: + index: "{{ item.0 }}" + + - name: Generate QR codes + shell: > + umask 077; + which segno && + segno --scale=5 --output={{ item.1 }}.png \ + "{{ lookup('template', 'client.conf.j2') }}" || true + changed_when: false + with_indexed_items: "{{ wireguard_users }}" + when: item.1 in users + vars: + index: "{{ item.0 }}" + ansible_python_interpreter: "{{ ansible_playbook_python }}" + args: + chdir: "{{ wireguard_config_path }}" + executable: bash + become: false + delegate_to: localhost + + - name: WireGuard configured + template: + src: server.conf.j2 + dest: "{{ config_prefix|default('/') }}etc/wireguard/{{ wireguard_interface }}.conf" + mode: "0600" + notify: restart wireguard tags: update-users -- name: WireGuard users config generated - template: - src: client.conf.j2 - dest: "{{ wireguard_config_path }}/{{ item.1 }}.conf" - mode: "0600" - with_indexed_items: "{{ users }}" - become: false - tags: update-users - delegate_to: localhost - -- name: Generate QR codes - shell: > - umask 077; - which segno && - segno --scale=5 --output={{ item.1 }}.png \ - "{{ lookup('template', 'client.conf.j2') }}" || true - changed_when: false - with_indexed_items: "{{ users }}" - delegate_to: localhost - become: false - tags: update-users - vars: - ansible_python_interpreter: "{{ ansible_playbook_python }}" - args: - chdir: "{{ wireguard_config_path }}" - executable: bash - name: WireGuard enabled and started service: diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index d7645be7..05bdea00 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -1,6 +1,6 @@ [Interface] PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} -Address = {{ lookup('file', wireguard_config_path + '/ip/' + item.1) }} +Address = {{ wireguard_client_ip }} DNS = {{ wireguard_dns_servers }} [Peer] diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index a2307d87..eb77f13a 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -4,10 +4,14 @@ ListenPort = {{ wireguard_port }} PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} SaveConfig = false -{% for u in users|sort %} +{% for u in wireguard_users %} +{% if u in users %} +{% set index = loop.index %} [Peer] # {{ u }} PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} -AllowedIPs = {{ lookup('file', wireguard_config_path + '/ip/' + u) }} +AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index }}/128{% endif %} + +{% endif %} {% endfor %} From affadd401d1a28ef3199e993441d687632b5e272 Mon Sep 17 00:00:00 2001 From: jxn Date: Tue, 13 Nov 2018 23:57:55 -0600 Subject: [PATCH 708/769] fix typos in docker documentation and shell-script text (#1202) --- algo-docker.sh | 2 +- docs/Docker.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/algo-docker.sh b/algo-docker.sh index da458034..858f6204 100644 --- a/algo-docker.sh +++ b/algo-docker.sh @@ -11,7 +11,7 @@ usage() { retcode="${1:-0}" echo "To run algo from Docker:" echo "" - echo "docker run --cap-drop ALL -it -v :"${DATA_DIR}" trailofbits/algo:latest" + echo "docker run --cap-drop=all -it -v :"${DATA_DIR}" trailofbits/algo:latest" echo "" exit ${retcode} } diff --git a/docs/Docker.md b/docs/Docker.md index 65f363b9..2efd5e32 100644 --- a/docs/Docker.md +++ b/docs/Docker.md @@ -22,7 +22,7 @@ While it is not possible to run your Algo server from within a Docker container, ``` - From Linux: ```bash - $ docker run --cap-drop-all -it \ + $ docker run --cap-drop=all -it \ -v /home/trailofbits/Documents/VPNs:/data \ trailofbits/algo:latest ``` From 1c16554b41eb45608da76a6c70c99d0be568b34b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 15 Nov 2018 10:22:11 +0100 Subject: [PATCH 709/769] Rename Docker.md to deploy-from-docker.md --- docs/{Docker.md => deploy-from-docker.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{Docker.md => deploy-from-docker.md} (100%) diff --git a/docs/Docker.md b/docs/deploy-from-docker.md similarity index 100% rename from docs/Docker.md rename to docs/deploy-from-docker.md From d8b318b59a5629e260d4d811785c11f744756826 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 16 Nov 2018 01:22:57 -0500 Subject: [PATCH 710/769] Detect when running in Docker (#1204) --- algo-showenv.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/algo-showenv.sh b/algo-showenv.sh index 41a6ff06..4793be9f 100755 --- a/algo-showenv.sh +++ b/algo-showenv.sh @@ -46,6 +46,8 @@ if [[ -x $(command -v systemd-detect-virt) ]]; then if [[ ${DETECT_VIRT} != "none" ]]; then VIRTUALIZED=" (Virtualized: ${DETECT_VIRT})" fi +elif [[ -f /.dockerenv ]]; then + VIRTUALIZED=" (Virtualized: docker)" fi echo "Algo running on: ${OS}${VIRTUALIZED}" From a4e4e2182ae9e448d3dc812081f398fa1830bb06 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 20 Nov 2018 11:38:53 -0500 Subject: [PATCH 711/769] Update mobileconfig.j2 Changes Child_SA lifetime to 2 hours, and IKE_SA lifetime to 12 hrs (Apple default is actually 12h for both). --- roles/vpn/templates/mobileconfig.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 54614fd4..0f4121df 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -66,7 +66,7 @@ IntegrityAlgorithm SHA2-512 LifeTimeInMinutes - 20 + 120 DeadPeerDetectionRate Medium @@ -87,7 +87,7 @@ IntegrityAlgorithm SHA2-512 LifeTimeInMinutes - 20 + 1440 LocalIdentifier {{ item.0 }} From fb93ed75abf1bcf6bccfe42a3ccf859075e2dff1 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Tue, 20 Nov 2018 11:56:42 -0500 Subject: [PATCH 712/769] Change server-side ipsec.conf settings Switching to inline rekeying from reauthentication, and lengthening child_SA and IKE_SA lifetimes. --- roles/vpn/templates/ipsec.conf.j2 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/vpn/templates/ipsec.conf.j2 index 086e18af..2320f4ec 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/vpn/templates/ipsec.conf.j2 @@ -4,11 +4,14 @@ config setup conn %default fragmentation=yes - rekey=no + rekey=yes + reauth=no dpdaction=clear keyexchange=ikev2 compress=yes dpddelay=35s + lifetime=3h + ikelifetime=12h {% if algo_windows %} ike={{ ciphers.compat.ike }} From 45b00ee994e2f5c1783a73149647fc93635c3cee Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 20 Nov 2018 19:20:24 +0100 Subject: [PATCH 713/769] BSD StrongSwan fixes (#1207) --- roles/common/tasks/freebsd.yml | 2 ++ roles/vpn/defaults/main.yml | 2 ++ roles/vpn/tasks/main.yml | 8 ++++++++ roles/vpn/templates/strongswan.conf.j2 | 15 ++++++++------- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index dc52931c..dda5dcf9 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,6 +1,8 @@ --- - set_fact: config_prefix: "/usr/local/" + strongswan_shell: /usr/sbin/nologin + strongswan_home: /var/empty root_group: wheel ssh_service_name: sshd apparmor_enabled: false diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index 8e044f29..a865dfb4 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -1,4 +1,6 @@ --- +strongswan_shell: /usr/sbin/nologin +strongswan_home: /var/lib/strongswan BetweenClients_DROP: true wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" wireguard_interface: wg0 diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml index 27be701a..bfe929ca 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/vpn/tasks/main.yml @@ -9,6 +9,14 @@ - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' + - name: Ensure that the strongswan user exist + user: + name: strongswan + group: nogroup + shell: "{{ strongswan_shell }}" + home: "{{ strongswan_home }}" + state: present + - name: Install strongSwan package: name=strongswan state=present diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/vpn/templates/strongswan.conf.j2 index 7fcf9ef4..f71c779e 100644 --- a/roles/vpn/templates/strongswan.conf.j2 +++ b/roles/vpn/templates/strongswan.conf.j2 @@ -13,13 +13,14 @@ charon { group = nogroup {% if ansible_distribution == 'FreeBSD' %} filelog { - /var/log/charon.log { - time_format = %b %e %T - ike_name = yes - append = no - default = 1 - flush_line = yes - } + charon { + path = /var/log/charon.log + time_format = %b %e %T + ike_name = yes + append = no + default = 1 + flush_line = yes + } } {% endif %} } From 9187d8e63752a6bb4ba12f38907a2a96432842ed Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 22 Nov 2018 19:04:37 +0100 Subject: [PATCH 714/769] dnscrypt-proxy apparmor fix (#1210) ## Description Apparmor profile for dnscrypt-proxy didn't work at all ## Motivation and Context Fixes #1155 ## How Has This Been Tested? Deployed to DigitalOcean, checked that the dnscrypt-proxy binary is in enforce mode ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] I have read the **CONTRIBUTING** document. - [x] My code follows the code style of this project. - [x] All new and existing tests passed. --- .../files/apparmor.profile.dnscrypt-proxy | 18 ++++++++++++------ roles/dns_encryption/tasks/ubuntu.yml | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy index a2e51639..7e900bc5 100644 --- a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -1,6 +1,6 @@ #include -/usr/sbin/dnscrypt-proxy { +/usr/bin/dnscrypt-proxy { #include #include #include @@ -12,12 +12,18 @@ capability setuid, capability sys_resource, - /etc/dnscrypt-proxy.toml r, + /etc/dnscrypt-proxy/** r, + /usr/bin/dnscrypt-proxy mr, + /tmp/public-resolvers.md* rw, + + /tmp/*.tmp w, + owner /tmp/*.tmp r, + + /run/systemd/notify rw, + /lib/x86_64-linux-gnu/ld-*.so mr, + @{PROC}/sys/kernel/hostname r, + @{PROC}/sys/net/core/somaxconn r, /etc/ld.so.cache r, - /usr/sbin/dnscrypt-proxy mr, - /usr/share/dnscrypt-proxy/dnscrypt-resolvers.csv r, /usr/local/lib/{@{multiarch}/,}libldns.so* mr, /usr/local/lib/{@{multiarch}/,}libsodium.so* mr, - /run/dnscrypt-proxy.pid rw, - /run/systemd/notify rw, } diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 13ba1709..89515ddb 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -27,14 +27,14 @@ - name: Ubuntu | Unbound profile for apparmor configured copy: src: apparmor.profile.dnscrypt-proxy - dest: /etc/apparmor.d/usr.sbin.dnscrypt-proxy + dest: /etc/apparmor.d/usr.bin.dnscrypt-proxy owner: root group: root mode: 0600 notify: restart dnscrypt-proxy - name: Ubuntu | Enforce the dnscrypt-proxy AppArmor policy - command: aa-enforce usr.sbin.dnscrypt-proxy + command: aa-enforce usr.bin.dnscrypt-proxy changed_when: false tags: apparmor when: apparmor_enabled|default(false)|bool == true From a66d8f00697029d95f7c35b0b33c827cd8d9cca0 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 22 Nov 2018 19:04:58 +0100 Subject: [PATCH 715/769] on-build python venvs (#1199) --- .gitignore | 2 + Dockerfile | 2 +- README.md | 2 +- config.cfg | 3 + playbooks/cloud-pre.yml | 10 ++ requirements.txt | 14 +- roles/cloud-azure/defaults/main.yml | 1 + roles/cloud-azure/tasks/main.yml | 73 +++++---- roles/cloud-azure/tasks/venv.yml | 32 ++++ roles/cloud-digitalocean/defaults/main.yml | 2 + roles/cloud-digitalocean/tasks/main.yml | 176 +++++++++++---------- roles/cloud-digitalocean/tasks/venv.yml | 13 ++ roles/cloud-ec2/defaults/main.yml | 1 + roles/cloud-ec2/tasks/main.yml | 66 ++++---- roles/cloud-ec2/tasks/venv.yml | 15 ++ roles/cloud-gce/defaults/main.yml | 2 + roles/cloud-gce/tasks/main.yml | 94 +++++------ roles/cloud-gce/tasks/venv.yml | 15 ++ roles/cloud-lightsail/defaults/main.yml | 2 + roles/cloud-lightsail/tasks/main.yml | 76 +++++---- roles/cloud-lightsail/tasks/venv.yml | 15 ++ roles/cloud-openstack/defaults/main.yml | 2 + roles/cloud-openstack/tasks/main.yml | 130 +++++++-------- roles/cloud-openstack/tasks/venv.yml | 13 ++ users.yml | 10 ++ venvs/.gitinit | 0 26 files changed, 466 insertions(+), 305 deletions(-) create mode 100644 roles/cloud-azure/tasks/venv.yml create mode 100644 roles/cloud-digitalocean/defaults/main.yml create mode 100644 roles/cloud-digitalocean/tasks/venv.yml create mode 100644 roles/cloud-ec2/tasks/venv.yml create mode 100644 roles/cloud-gce/defaults/main.yml create mode 100644 roles/cloud-gce/tasks/venv.yml create mode 100644 roles/cloud-lightsail/defaults/main.yml create mode 100644 roles/cloud-lightsail/tasks/venv.yml create mode 100644 roles/cloud-openstack/defaults/main.yml create mode 100644 roles/cloud-openstack/tasks/venv.yml create mode 100644 venvs/.gitinit diff --git a/.gitignore b/.gitignore index b632022a..de4fd233 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ inventory_users *.kate-swp env .DS_Store +venvs/* +!venvs/.gitinit diff --git a/Dockerfile b/Dockerfile index c2476ae1..6fa1d0fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY . . RUN chmod 0755 /algo/algo-docker.sh # Because of the bind mounting of `configs/`, we need to run as the `root` user -# This may break in cases where user namespacing is enabled, so hopefully Docker +# This may break in cases where user namespacing is enabled, so hopefully Docker # sorts out a way to set permissions on bind-mounted volumes (`docker run -v`) # before userns becomes default # Note that not running as root will break if we don't have a matching userid diff --git a/README.md b/README.md index 282de737..8737d5da 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ The easiest way to get an Algo server running is to let it set up a _new_ virtua ```bash $ python -m virtualenv --python=`which python2` env && source env/bin/activate && - python -m pip install -U pip && + python -m pip install -U pip virtualenv && python -m pip install -r requirements.txt ``` On macOS, you may be prompted to install `cc`. You should press accept if so. diff --git a/config.cfg b/config.cfg index 03f439e9..7f46aa54 100644 --- a/config.cfg +++ b/config.cfg @@ -13,6 +13,9 @@ users: # If True re-init all existing certificates. Boolean keys_clean_all: False +# Clean up cloud python environments +clean_environment: false + vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index b40f6c85..338e70dd 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -14,6 +14,16 @@ 'dns_encryption "{{ dns_encryption }}"' \ > /dev/tty +- name: Install the requirements + local_action: + module: pip + state: latest + name: + - pyOpenSSL + - jinja2==2.8 + - segno + tags: always + - name: Generate the SSH private key openssl_privatekey: path: "{{ SSH_keys.private }}" diff --git a/requirements.txt b/requirements.txt index 4d40c39b..38f36dac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1 @@ -setuptools>=11.3 -SecretStorage < 3 -ansible[azure]==2.5.2 -dopy==0.3.5 -boto>=2.5 -boto3 -apache-libcloud -six -pyopenssl -jinja2==2.8 -shade -pycrypto -segno +ansible==2.5.2 diff --git a/roles/cloud-azure/defaults/main.yml b/roles/cloud-azure/defaults/main.yml index cd5301d2..dbd82f36 100644 --- a/roles/cloud-azure/defaults/main.yml +++ b/roles/cloud-azure/defaults/main.yml @@ -1,4 +1,5 @@ --- +azure_venv: "{{ playbook_dir }}/configs/.venvs/azure" _azure_regions: > [ { diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 27e2defc..38adc741 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -1,43 +1,48 @@ --- - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.yml - - set_fact: - algo_region: >- - {% if region is defined %}{{ region }} - {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ azure_regions[_algo_region.user_input | int -1 ]['name'] }} - {%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} + - block: + - name: Include prompts + import_tasks: prompts.yml - - name: Create AlgoVPN Server - azure_rm_deployment: - state: present - deployment_name: "AlgoVPN-{{ algo_server_name }}" - template: "{{ lookup('file', 'deployment.json') }}" - secret: "{{ secret }}" - tenant: "{{ tenant }}" - client_id: "{{ client_id }}" - subscription_id: "{{ subscription_id }}" - resource_group_name: "AlgoVPN-{{ algo_server_name }}" - parameters: - AlgoServerName: - value: "{{ algo_server_name }}" - sshKeyData: - value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - location: - value: "{{ algo_region }}" - WireGuardPort: - value: "{{ wireguard_port }}" - vmSize: - value: "{{ cloud_providers.azure.size }}" - imageReferenceSku: - value: "{{ cloud_providers.azure.image }}" - register: azure_rm_deployment + - set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ azure_regions[_algo_region.user_input | int -1 ]['name'] }} + {%- else %}{{ azure_regions[default_region | int - 1]['name'] }}{% endif %} - - set_fact: - cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" - ansible_ssh_user: ubuntu + - name: Create AlgoVPN Server + azure_rm_deployment: + state: present + deployment_name: "AlgoVPN-{{ algo_server_name }}" + template: "{{ lookup('file', 'deployment.json') }}" + secret: "{{ secret }}" + tenant: "{{ tenant }}" + client_id: "{{ client_id }}" + subscription_id: "{{ subscription_id }}" + resource_group_name: "AlgoVPN-{{ algo_server_name }}" + parameters: + AlgoServerName: + value: "{{ algo_server_name }}" + sshKeyData: + value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + location: + value: "{{ algo_region }}" + WireGuardPort: + value: "{{ wireguard_port }}" + vmSize: + value: "{{ cloud_providers.azure.size }}" + imageReferenceSku: + value: "{{ cloud_providers.azure.image }}" + register: azure_rm_deployment + - set_fact: + cloud_instance_ip: "{{ azure_rm_deployment.deployment.outputs.publicIPAddresses.value }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ azure_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-azure/tasks/venv.yml b/roles/cloud-azure/tasks/venv.yml new file mode 100644 index 00000000..cbadf8de --- /dev/null +++ b/roles/cloud-azure/tasks/venv.yml @@ -0,0 +1,32 @@ +--- +- name: Clean up the environment + file: + dest: "{{ azure_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - packaging + - requests[security] + - azure-mgmt-compute>=2.0.0,<3 + - azure-mgmt-network>=1.3.0,<2 + - azure-mgmt-storage>=1.5.0,<2 + - azure-mgmt-resource>=1.1.0,<2 + - azure-storage>=0.35.1,<0.36 + - azure-cli-core>=2.0.12,<3 + - msrest==0.4.29 + - msrestazure==0.4.31 + - azure-mgmt-dns>=1.0.1,<2 + - azure-mgmt-keyvault>=0.40.0,<0.41 + - azure-mgmt-batch>=4.1.0,<5 + - azure-mgmt-sql>=0.7.1,<0.8 + - azure-mgmt-web>=0.32.0,<0.33 + - azure-mgmt-containerservice>=2.0.0,<3.0.0 + - azure-mgmt-containerregistry>=1.0.1 + - azure-mgmt-rdbms==1.2.0 + - azure-mgmt-containerinstance==0.4.0 + state: latest + virtualenv: "{{ azure_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-digitalocean/defaults/main.yml b/roles/cloud-digitalocean/defaults/main.yml new file mode 100644 index 00000000..34ba5f86 --- /dev/null +++ b/roles/cloud-digitalocean/defaults/main.yml @@ -0,0 +1,2 @@ +--- +digitalocean_venv: "{{ playbook_dir }}/configs/.venvs/digitalocean" diff --git a/roles/cloud-digitalocean/tasks/main.yml b/roles/cloud-digitalocean/tasks/main.yml index aca66b7b..488ea2d1 100644 --- a/roles/cloud-digitalocean/tasks/main.yml +++ b/roles/cloud-digitalocean/tasks/main.yml @@ -1,102 +1,108 @@ - block: - - name: Include prompts - import_tasks: prompts.yml - - - name: Set additional facts - set_fact: - algo_do_region: >- - {% if region is defined %}{{ region }} - {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ do_regions[_algo_region.user_input | int -1 ]['slug'] }} - {%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} - public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" + - name: Build python virtual environment + import_tasks: venv.yml - block: - - name: "Delete the existing Algo SSH keys" - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - until: ssh_keys.changed != true - retries: 10 - delay: 1 + - name: Include prompts + import_tasks: prompts.yml - rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes + - name: Set additional facts + set_fact: + algo_do_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ do_regions[_algo_region.user_input | int -1 ]['slug'] }} + {%- else %}{{ do_regions[default_region | int - 1]['slug'] }}{% endif %} + public_key: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - - debug: var=ssh_keys + - block: + - name: "Delete the existing Algo SSH keys" + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - fail: - msg: "Please, ensure that your API token is not read-only." + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes - - name: "Upload the SSH key" - digital_ocean: - state: present - command: ssh - ssh_pub_key: "{{ public_key }}" - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: do_ssh_key + - debug: var=ssh_keys - - name: "Creating a droplet..." - digital_ocean: - state: present - command: droplet - name: "{{ algo_server_name }}" - region_id: "{{ algo_do_region }}" - size_id: "{{ cloud_providers.digitalocean.size }}" - image_id: "{{ cloud_providers.digitalocean.image }}" - ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" - unique_name: yes - api_token: "{{ algo_do_token }}" - ipv6: yes - register: do + - fail: + msg: "Please, ensure that your API token is not read-only." - - set_fact: - cloud_instance_ip: "{{ do.droplet.ip_address }}" - ansible_ssh_user: root + - name: "Upload the SSH key" + digital_ocean: + state: present + command: ssh + ssh_pub_key: "{{ public_key }}" + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: do_ssh_key - - name: Tag the droplet - digital_ocean_tag: - name: "Environment:Algo" - resource_id: "{{ do.droplet.id }}" - api_token: "{{ algo_do_token }}" - state: present + - name: "Creating a droplet..." + digital_ocean: + state: present + command: droplet + name: "{{ algo_server_name }}" + region_id: "{{ algo_do_region }}" + size_id: "{{ cloud_providers.digitalocean.size }}" + image_id: "{{ cloud_providers.digitalocean.image }}" + ssh_key_ids: "{{ do_ssh_key.ssh_key.id }}" + unique_name: yes + api_token: "{{ algo_do_token }}" + ipv6: yes + register: do - - block: - - name: "Delete the new Algo SSH key" - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - until: ssh_keys.changed != true - retries: 10 - delay: 1 + - set_fact: + cloud_instance_ip: "{{ do.droplet.ip_address }}" + ansible_ssh_user: root - rescue: - - name: Collect the fail error - digital_ocean: - state: absent - command: ssh - api_token: "{{ algo_do_token }}" - name: "{{ SSH_keys.comment }}" - register: ssh_keys - ignore_errors: yes + - name: Tag the droplet + digital_ocean_tag: + name: "Environment:Algo" + resource_id: "{{ do.droplet.id }}" + api_token: "{{ algo_do_token }}" + state: present - - debug: var=ssh_keys + - block: + - name: "Delete the new Algo SSH key" + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + until: ssh_keys.changed != true + retries: 10 + delay: 1 - - fail: - msg: "Please, ensure that your API token is not read-only." + rescue: + - name: Collect the fail error + digital_ocean: + state: absent + command: ssh + api_token: "{{ algo_do_token }}" + name: "{{ SSH_keys.comment }}" + register: ssh_keys + ignore_errors: yes + + - debug: var=ssh_keys + + - fail: + msg: "Please, ensure that your API token is not read-only." + environment: + PYTHONPATH: "{{ digitalocean_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-digitalocean/tasks/venv.yml b/roles/cloud-digitalocean/tasks/venv.yml new file mode 100644 index 00000000..80e85b9f --- /dev/null +++ b/roles/cloud-digitalocean/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ digitalocean_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: dopy + version: 0.3.5 + virtualenv: "{{ digitalocean_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-ec2/defaults/main.yml b/roles/cloud-ec2/defaults/main.yml index 8060eb72..12b3f19d 100644 --- a/roles/cloud-ec2/defaults/main.yml +++ b/roles/cloud-ec2/defaults/main.yml @@ -4,3 +4,4 @@ encrypted: "{{ cloud_providers.ec2.encrypted }}" ec2_vpc_nets: cidr_block: 172.16.0.0/16 subnet_cidr: 172.16.254.0/23 +ec2_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-ec2/tasks/main.yml b/roles/cloud-ec2/tasks/main.yml index 64dbfcd4..ea3a67a4 100644 --- a/roles/cloud-ec2/tasks/main.yml +++ b/roles/cloud-ec2/tasks/main.yml @@ -1,40 +1,46 @@ - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.yml - - set_fact: - algo_region: >- - {% if region is defined %}{{ region }} - {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ aws_regions[_algo_region.user_input | int -1 ]['region_name'] }} - {%- else %}{{ aws_regions[default_region | int - 1]['region_name'] }}{% endif %} - stack_name: "{{ algo_server_name | replace('.', '-') }}" + - block: + - name: Include prompts + import_tasks: prompts.yml - - name: Locate official AMI for region - ec2_ami_facts: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - owners: "{{ cloud_providers.ec2.image.owner }}" - region: "{{ algo_region }}" - filters: - name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" - register: ami_search + - set_fact: + algo_region: >- + {% if region is defined %}{{ region }} + {%- elif _algo_region.user_input is defined and _algo_region.user_input != "" %}{{ aws_regions[_algo_region.user_input | int -1 ]['region_name'] }} + {%- else %}{{ aws_regions[default_region | int - 1]['region_name'] }}{% endif %} + stack_name: "{{ algo_server_name | replace('.', '-') }}" - - import_tasks: encrypt_image.yml - when: encrypted + - name: Locate official AMI for region + ec2_ami_facts: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + owners: "{{ cloud_providers.ec2.image.owner }}" + region: "{{ algo_region }}" + filters: + name: "ubuntu/images/hvm-ssd/{{ cloud_providers.ec2.image.name }}-amd64-server-*" + register: ami_search - - name: Set the ami id as a fact - set_fact: - ami_image: >- - {% if ami_search_encrypted.image_id is defined %}{{ ami_search_encrypted.image_id }} - {%- elif search_crypt.images is defined and search_crypt.images|length >= 1 %}{{ (search_crypt.images | sort(attribute='creation_date') | last)['image_id'] }} - {%- else %}{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}{% endif %} + - import_tasks: encrypt_image.yml + when: encrypted - - name: Deploy the stack - import_tasks: cloudformation.yml + - name: Set the ami id as a fact + set_fact: + ami_image: >- + {% if ami_search_encrypted.image_id is defined %}{{ ami_search_encrypted.image_id }} + {%- elif search_crypt.images is defined and search_crypt.images|length >= 1 %}{{ (search_crypt.images | sort(attribute='creation_date') | last)['image_id'] }} + {%- else %}{{ (ami_search.images | sort(attribute='creation_date') | last)['image_id'] }}{% endif %} - - set_fact: - cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" - ansible_ssh_user: ubuntu + - name: Deploy the stack + import_tasks: cloudformation.yml + + - set_fact: + cloud_instance_ip: "{{ stack.stack_outputs.ElasticIP }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ ec2_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-ec2/tasks/venv.yml b/roles/cloud-ec2/tasks/venv.yml new file mode 100644 index 00000000..be2eeced --- /dev/null +++ b/roles/cloud-ec2/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ ec2_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ ec2_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-gce/defaults/main.yml b/roles/cloud-gce/defaults/main.yml new file mode 100644 index 00000000..d771cc8f --- /dev/null +++ b/roles/cloud-gce/defaults/main.yml @@ -0,0 +1,2 @@ +--- +gce_venv: "{{ playbook_dir }}/configs/.venvs/gce" diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index 8af6ff87..e04b3d80 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -1,54 +1,60 @@ - block: - - name: Include prompts - import_tasks: prompts.yml - - - name: Network configured - gce_net: - name: "algo-net-{{ algo_server_name }}" - fwname: "algo-net-{{ algo_server_name }}-fw" - allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" - state: "present" - mode: auto - src_range: 0.0.0.0/0 - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" + - name: Build python virtual environment + import_tasks: venv.yml - block: - - name: External IP allocated - gce_eip: + - name: Include prompts + import_tasks: prompts.yml + + - name: Network configured + gce_net: + name: "algo-net-{{ algo_server_name }}" + fwname: "algo-net-{{ algo_server_name }}-fw" + allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" + state: "present" + mode: auto + src_range: 0.0.0.0/0 + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + + - block: + - name: External IP allocated + gce_eip: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file_path }}" + project_id: "{{ project_id }}" + name: "{{ algo_server_name }}" + region: "{{ algo_region.split('-')[0:2] | join('-') }}" + state: present + register: gce_eip + + - name: Set External IP as a fact + set_fact: + external_ip: "{{ gce_eip.address }}" + when: cloud_providers.gce.external_static_ip + + - name: "Creating a new instance..." + gce: + instance_names: "{{ algo_server_name }}" + zone: "{{ algo_region }}" + external_ip: "{{ external_ip | default('ephemeral') }}" + machine_type: "{{ cloud_providers.gce.size }}" + image: "{{ cloud_providers.gce.image }}" service_account_email: "{{ service_account_email }}" credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" - name: "{{ algo_server_name }}" - region: "{{ algo_region.split('-')[0:2] | join('-') }}" - state: present - register: gce_eip + metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' + network: "algo-net-{{ algo_server_name }}" + tags: + - "environment-algo" + register: google_vm - - name: Set External IP as a fact - set_fact: - external_ip: "{{ gce_eip.address }}" - when: cloud_providers.gce.external_static_ip - - - name: "Creating a new instance..." - gce: - instance_names: "{{ algo_server_name }}" - zone: "{{ algo_region }}" - external_ip: "{{ external_ip | default('ephemeral') }}" - machine_type: "{{ cloud_providers.gce.size }}" - image: "{{ cloud_providers.gce.image }}" - service_account_email: "{{ service_account_email }}" - credentials_file: "{{ credentials_file_path }}" - project_id: "{{ project_id }}" - metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ algo_server_name }}" - tags: - - "environment-algo" - register: google_vm - - - set_fact: - cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" - ansible_ssh_user: ubuntu + - set_fact: + cloud_instance_ip: "{{ google_vm.instance_data[0].public_ip }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ gce_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint tags: always diff --git a/roles/cloud-gce/tasks/venv.yml b/roles/cloud-gce/tasks/venv.yml new file mode 100644 index 00000000..078efe5b --- /dev/null +++ b/roles/cloud-gce/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ gce_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - apache-libcloud + - pycrypto + state: latest + virtualenv: "{{ gce_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-lightsail/defaults/main.yml b/roles/cloud-lightsail/defaults/main.yml new file mode 100644 index 00000000..06ae0ee9 --- /dev/null +++ b/roles/cloud-lightsail/defaults/main.yml @@ -0,0 +1,2 @@ +--- +lightsail_venv: "{{ playbook_dir }}/configs/.venvs/aws" diff --git a/roles/cloud-lightsail/tasks/main.yml b/roles/cloud-lightsail/tasks/main.yml index 29342af9..21e3d459 100644 --- a/roles/cloud-lightsail/tasks/main.yml +++ b/roles/cloud-lightsail/tasks/main.yml @@ -1,41 +1,47 @@ - block: - - name: Include prompts - import_tasks: prompts.yml + - name: Build python virtual environment + import_tasks: venv.yml - - name: Create an instance - lightsail: - aws_access_key: "{{ access_key }}" - aws_secret_key: "{{ secret_key }}" - name: "{{ algo_server_name }}" - state: present - region: "{{ algo_region }}" - zone: "{{ algo_region }}a" - blueprint_id: "{{ cloud_providers.lightsail.image }}" - bundle_id: "{{ cloud_providers.lightsail.size }}" - wait_timeout: 300 - open_ports: - - from_port: 4500 - to_port: 4500 - protocol: udp - - from_port: 500 - to_port: 500 - protocol: udp - - from_port: "{{ wireguard_port }}" - to_port: "{{ wireguard_port }}" - protocol: udp - user_data: | - #!/bin/bash - mkdir -p /home/ubuntu/.ssh/ - echo "{{ lookup('file', '{{ SSH_keys.public }}') }}" >> /home/ubuntu/.ssh/authorized_keys - chown -R ubuntu: /home/ubuntu/.ssh/ - chmod 0700 /home/ubuntu/.ssh/ - chmod 0600 /home/ubuntu/.ssh/* - test - register: algo_instance + - block: + - name: Include prompts + import_tasks: prompts.yml - - set_fact: - cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" - ansible_ssh_user: ubuntu + - name: Create an instance + lightsail: + aws_access_key: "{{ access_key }}" + aws_secret_key: "{{ secret_key }}" + name: "{{ algo_server_name }}" + state: present + region: "{{ algo_region }}" + zone: "{{ algo_region }}a" + blueprint_id: "{{ cloud_providers.lightsail.image }}" + bundle_id: "{{ cloud_providers.lightsail.size }}" + wait_timeout: 300 + open_ports: + - from_port: 4500 + to_port: 4500 + protocol: udp + - from_port: 500 + to_port: 500 + protocol: udp + - from_port: "{{ wireguard_port }}" + to_port: "{{ wireguard_port }}" + protocol: udp + user_data: | + #!/bin/bash + mkdir -p /home/ubuntu/.ssh/ + echo "{{ lookup('file', '{{ SSH_keys.public }}') }}" >> /home/ubuntu/.ssh/authorized_keys + chown -R ubuntu: /home/ubuntu/.ssh/ + chmod 0700 /home/ubuntu/.ssh/ + chmod 0600 /home/ubuntu/.ssh/* + test + register: algo_instance + + - set_fact: + cloud_instance_ip: "{{ algo_instance['instance']['public_ip_address'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ lightsail_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint diff --git a/roles/cloud-lightsail/tasks/venv.yml b/roles/cloud-lightsail/tasks/venv.yml new file mode 100644 index 00000000..9816fea1 --- /dev/null +++ b/roles/cloud-lightsail/tasks/venv.yml @@ -0,0 +1,15 @@ +--- +- name: Clean up the environment + file: + dest: "{{ lightsail_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: + - boto>=2.5 + - boto3 + state: latest + virtualenv: "{{ lightsail_venv }}" + virtualenv_python: python2.7 diff --git a/roles/cloud-openstack/defaults/main.yml b/roles/cloud-openstack/defaults/main.yml new file mode 100644 index 00000000..3bec06b2 --- /dev/null +++ b/roles/cloud-openstack/defaults/main.yml @@ -0,0 +1,2 @@ +--- +openstack_venv: "{{ playbook_dir }}/configs/.venvs/openstack" diff --git a/roles/cloud-openstack/tasks/main.yml b/roles/cloud-openstack/tasks/main.yml index 8fb1e6b0..75b3db6d 100644 --- a/roles/cloud-openstack/tasks/main.yml +++ b/roles/cloud-openstack/tasks/main.yml @@ -4,77 +4,83 @@ when: lookup('env', 'OS_AUTH_URL') == "" - block: - - name: Security group created - os_security_group: - state: "{{ state|default('present') }}" - name: "{{ algo_server_name }}-security_group" - description: AlgoVPN security group - register: os_security_group + - name: Build python virtual environment + import_tasks: venv.yml - - name: Security rules created - os_security_group_rule: - state: "{{ state|default('present') }}" - security_group: "{{ os_security_group.id }}" - protocol: "{{ item.proto }}" - port_range_min: "{{ item.port_min }}" - port_range_max: "{{ item.port_max }}" - remote_ip_prefix: "{{ item.range }}" - with_items: - - { proto: tcp, port_min: 22, port_max: 22, range: 0.0.0.0/0 } - - { proto: icmp, port_min: -1, port_max: -1, range: 0.0.0.0/0 } - - { proto: udp, port_min: 4500, port_max: 4500, range: 0.0.0.0/0 } - - { proto: udp, port_min: 500, port_max: 500, range: 0.0.0.0/0 } - - { proto: udp, port_min: "{{ wireguard_port }}", port_max: "{{ wireguard_port }}", range: 0.0.0.0/0 } + - block: + - name: Security group created + os_security_group: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}-security_group" + description: AlgoVPN security group + register: os_security_group - - name: Keypair created - os_keypair: - state: "{{ state|default('present') }}" - name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" - public_key_file: "{{ SSH_keys.public }}" - register: os_keypair + - name: Security rules created + os_security_group_rule: + state: "{{ state|default('present') }}" + security_group: "{{ os_security_group.id }}" + protocol: "{{ item.proto }}" + port_range_min: "{{ item.port_min }}" + port_range_max: "{{ item.port_max }}" + remote_ip_prefix: "{{ item.range }}" + with_items: + - { proto: tcp, port_min: 22, port_max: 22, range: 0.0.0.0/0 } + - { proto: icmp, port_min: -1, port_max: -1, range: 0.0.0.0/0 } + - { proto: udp, port_min: 4500, port_max: 4500, range: 0.0.0.0/0 } + - { proto: udp, port_min: 500, port_max: 500, range: 0.0.0.0/0 } + - { proto: udp, port_min: "{{ wireguard_port }}", port_max: "{{ wireguard_port }}", range: 0.0.0.0/0 } - - name: Gather facts about flavors - os_flavor_facts: - ram: "{{ cloud_providers.openstack.flavor_ram }}" + - name: Keypair created + os_keypair: + state: "{{ state|default('present') }}" + name: "{{ SSH_keys.comment|regex_replace('@', '_') }}" + public_key_file: "{{ SSH_keys.public }}" + register: os_keypair - - name: Gather facts about images - os_image_facts: - image: "{{ cloud_providers.openstack.image }}" + - name: Gather facts about flavors + os_flavor_facts: + ram: "{{ cloud_providers.openstack.flavor_ram }}" - - name: Gather facts about public networks - os_networks_facts: + - name: Gather facts about images + os_image_facts: + image: "{{ cloud_providers.openstack.image }}" - - name: Set the network as a fact - set_fact: - public_network_id: "{{ item.id }}" - when: - - item['router:external']|default(omit) - - item['admin_state_up']|default(omit) - - item['status'] == 'ACTIVE' - with_items: "{{ openstack_networks }}" + - name: Gather facts about public networks + os_networks_facts: - - name: Set facts - set_fact: - flavor_id: "{{ (openstack_flavors | sort(attribute='ram'))[0]['id'] }}" - image_id: "{{ openstack_image['id'] }}" - keypair_name: "{{ os_keypair.key.name }}" - security_group_name: "{{ os_security_group['secgroup']['name'] }}" + - name: Set the network as a fact + set_fact: + public_network_id: "{{ item.id }}" + when: + - item['router:external']|default(omit) + - item['admin_state_up']|default(omit) + - item['status'] == 'ACTIVE' + with_items: "{{ openstack_networks }}" - - name: Server created - os_server: - state: "{{ state|default('present') }}" - name: "{{ algo_server_name }}" - image: "{{ image_id }}" - flavor: "{{ flavor_id }}" - key_name: "{{ keypair_name }}" - security_groups: "{{ security_group_name }}" - nics: - - net-id: "{{ public_network_id }}" - register: os_server + - name: Set facts + set_fact: + flavor_id: "{{ (openstack_flavors | sort(attribute='ram'))[0]['id'] }}" + image_id: "{{ openstack_image['id'] }}" + keypair_name: "{{ os_keypair.key.name }}" + security_group_name: "{{ os_security_group['secgroup']['name'] }}" - - set_fact: - cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" - ansible_ssh_user: ubuntu + - name: Server created + os_server: + state: "{{ state|default('present') }}" + name: "{{ algo_server_name }}" + image: "{{ image_id }}" + flavor: "{{ flavor_id }}" + key_name: "{{ keypair_name }}" + security_groups: "{{ security_group_name }}" + nics: + - net-id: "{{ public_network_id }}" + register: os_server + + - set_fact: + cloud_instance_ip: "{{ os_server['openstack']['public_v4'] }}" + ansible_ssh_user: ubuntu + environment: + PYTHONPATH: "{{ openstack_venv }}/lib/python2.7/site-packages/" rescue: - debug: var=fail_hint diff --git a/roles/cloud-openstack/tasks/venv.yml b/roles/cloud-openstack/tasks/venv.yml new file mode 100644 index 00000000..e2c4f86a --- /dev/null +++ b/roles/cloud-openstack/tasks/venv.yml @@ -0,0 +1,13 @@ +--- +- name: Clean up the environment + file: + dest: "{{ openstack_venv }}" + state: absent + when: clean_environment + +- name: Install requirements + pip: + name: shade + state: latest + virtualenv: "{{ openstack_venv }}" + virtualenv_python: python2.7 diff --git a/users.yml b/users.yml index bb934946..30e460ae 100644 --- a/users.yml +++ b/users.yml @@ -58,6 +58,16 @@ - config.cfg - "configs/{{ inventory_hostname }}/config.yml" + pre_tasks: + - block: + - name: Local pre-tasks + import_tasks: playbooks/cloud-pre.yml + rescue: + - debug: var=fail_hint + tags: always + - fail: + tags: always + roles: - role: common - role: wireguard diff --git a/venvs/.gitinit b/venvs/.gitinit new file mode 100644 index 00000000..e69de29b From 22395f5f84bde70a2fe0e6cda37c24ea4dc9bc33 Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 26 Nov 2018 10:58:34 -0500 Subject: [PATCH 716/769] Add p12 password back to mobileconfigs (#1218) --- roles/vpn/templates/mobileconfig.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 54614fd4..8a0bb5f6 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -134,6 +134,8 @@ IKEv2 + Password + {{ p12_export_password }} PayloadCertificateFileName {{ item.0 }}.p12 PayloadContent From adb4dfa839f5a98e46df29823900bfa1aeffa68c Mon Sep 17 00:00:00 2001 From: Jack Sullivan Date: Mon, 26 Nov 2018 22:09:33 -0800 Subject: [PATCH 717/769] Add "unable to write 'random state'" resolution (#1219) I ran into the same issue as #1058, and the solution worked. This PR generalizes the solution and adds it to the troubleshooting documentation, making it easier to resolve for future users. --- docs/troubleshooting.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 6e910218..e9335947 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -18,6 +18,7 @@ First of all, check [this](https://github.com/trailofbits/algo#features) and ens * [Windows: The value of parameter linuxConfiguration.ssh.publicKeys.keyData is invalid](#windows-the-value-of-parameter-linuxconfigurationsshpublickeyskeydata-is-invalid) * [Docker: Failed to connect to the host via ssh](#docker-failed-to-connect-to-the-host-via-ssh) * [Wireguard: Unable to find 'configs/...' in expected paths](#wireguard-unable-to-find-configs-in-expected-paths) + * [Ubuntu Error: "unable to write 'random state" when generating CA password](#ubuntu-error-unable-to-write-random-state-when-generating-ca-password") * [Connection Problems](#connection-problems) * [I'm blocked or get CAPTCHAs when I access certain websites](#im-blocked-or-get-captchas-when-i-access-certain-websites) * [I want to change the list of trusted Wifi networks on my Apple device](#i-want-to-change-the-list-of-trusted-wifi-networks-on-my-apple-device) @@ -268,6 +269,23 @@ sudo rm -rf /etc/wireguard/*.lock ``` Then immediately re-run `./algo`. +### Ubuntu Error: "unable to write 'random state" when generating CA password + +When running Algo, you received an error like this: + +``` +TASK [common : Generate password for the CA key] *********************************************************************************************************************************************************** +fatal: [xxx.xxx.xxx.xxx -> localhost]: FAILED! => {"changed": true, "cmd": "openssl rand -hex 16", "delta": "0:00:00.024776", "end": "2018-11-26 13:13:55.879921", "msg": "non-zero return code", "rc": 1, "start": "2018-11-26 13:13:55.855145", "stderr": "unable to write 'random state'", "stderr_lines": ["unable to write 'random state'"], "stdout": "xxxxxxxxxxxxxxxxxxx", "stdout_lines": ["xxxxxxxxxxxxxxxxxxx"]} +``` + +This happens when your user does not have ownership of the `$HOME/.rnd` file, which is a seed for randomization. To fix this issue, give your user ownership of the file with this command: + +``` +sudo chown $USER:$USER $HOME/.rnd +``` + +Now, run Algo again. + ## Connection Problems Look here if you deployed an Algo server but now have a problem connecting to it with a client. From 66bbf0e83a1c1c09fb06e438a84862bc4eb68174 Mon Sep 17 00:00:00 2001 From: jxn Date: Thu, 29 Nov 2018 07:11:26 -0600 Subject: [PATCH 718/769] fix typo in powershell execution in windows client set up doc (#1224) --- docs/client-windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 77ba3c6f..53b62f22 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -8,7 +8,7 @@ To install automatically, use the generated user Powershell script. 2. Open Powershell as Administrator. 3. Run the following command: ```powershell -powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 Add +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add ``` 4. The command has help information available. To view its full help, run this from Powershell: ```powershell From 8d23f715d7d403636242bed4ad6261404f5db881 Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 3 Dec 2018 09:33:36 -0500 Subject: [PATCH 719/769] Run adblock.sh at a random time (#1227) --- roles/dns_adblocking/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/dns_adblocking/tasks/main.yml b/roles/dns_adblocking/tasks/main.yml index b276d355..6a44dbee 100644 --- a/roles/dns_adblocking/tasks/main.yml +++ b/roles/dns_adblocking/tasks/main.yml @@ -30,8 +30,8 @@ - name: Adblock script added to cron cron: name: Adblock hosts update - minute: 10 - hour: 2 + minute: "{{ range(0, 60) | random }}" + hour: "{{ range(0, 24) | random }}" job: /usr/local/sbin/adblock.sh user: root From 5b59557a460e03d81a4ee76e12ade5a2b59792b3 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Mon, 3 Dec 2018 10:36:47 -0500 Subject: [PATCH 720/769] Update mobileconfig.j2 Not sure why I had it at 120 for this. I'm running fine with both IKE lifetime and CHILD_SA lifetimes at 1440 (Apple default). --- roles/vpn/templates/mobileconfig.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 0f4121df..d9ffbf3b 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -66,7 +66,7 @@ IntegrityAlgorithm SHA2-512 LifeTimeInMinutes - 120 + 1440 DeadPeerDetectionRate Medium From 66681521c156984da58320f83625154720cec2cf Mon Sep 17 00:00:00 2001 From: David Myers Date: Mon, 3 Dec 2018 12:32:23 -0500 Subject: [PATCH 721/769] Increase memory limit for dnsmasq (#1228) * Increase memory limit for dnsmasq * Increase memory limit for dnsmasq further --- roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 index 98cbbddb..30e5359b 100644 --- a/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 +++ b/roles/dns_adblocking/templates/100-CustomLimitations.conf.j2 @@ -1,4 +1,5 @@ [Service] -MemoryLimit=16777216 +MemoryHigh=128M +MemoryMax=192M CPUAccounting=true -CPUQuota=5% +CPUQuota=20% From 319b630cf417369896e021af098551f7784526df Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 5 Dec 2018 00:57:13 -0500 Subject: [PATCH 722/769] docs/gce: Fix typos, clarify instructions (#1239) --- docs/cloud-gce.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cloud-gce.md b/docs/cloud-gce.md index fe43c43a..c8467655 100644 --- a/docs/cloud-gce.md +++ b/docs/cloud-gce.md @@ -1,12 +1,12 @@ # Google Cloud Platform setup -Follow the [installation instructions](https://cloud.google.com/sdk/) to have the CLI commands to interact with Google. +* Follow the [`gcloud` installation instructions](https://cloud.google.com/sdk/) -After creating an account and installing, login in on your account using `gcloud init` +* Log into your account using `gcloud init` ### Creating a project -The recommendation on GCP is to group resources on **Projets**, so we will create one project to put our VPN server and service account restricted to it. +The recommendation on GCP is to group resources into **Projects**, so we will create a new project for our VPN server and use a service account restricted to it. ```bash ## Create the project to group the resources @@ -38,4 +38,4 @@ gcloud services enable compute.googleapis.com **Attention:** take care of the `configs/gce.json` file, which contains the credentials to manage your Google Cloud account, including create and delete servers on this project. -There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md) +There are more advanced arguments available for deploynment [using ansible](deploy-from-ansible.md). From 4eeaadcfb3f573a00ce4a610c9de34142d0db7e4 Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Fri, 7 Dec 2018 14:41:19 -0500 Subject: [PATCH 723/769] Add info about modifying blacklists (#1236) # Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: # /usr/local/sbin/adblock.sh # If you load very large blocklists, you may also have to modify resource limits: # /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf --- config.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config.cfg b/config.cfg index 7f46aa54..088ed012 100644 --- a/config.cfg +++ b/config.cfg @@ -35,6 +35,11 @@ wireguard_port: 51820 # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 +# Algo will use the following lists to block ads. You can add new block lists +# after deployment by modifying the line starting "BLOCKLIST_URLS=" at: +# /usr/local/sbin/adblock.sh +# If you load very large blocklists, you may also have to modify resource limits: +# /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf adblock_lists: - "http://winhelp2002.mvps.org/hosts.txt" - "https://adaway.org/hosts.txt" From f3519425c4335f2914a42f697cb4028fd6beb0e8 Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 7 Dec 2018 14:41:39 -0500 Subject: [PATCH 724/769] Note that WireGuard configs cannot be shared (#1238) --- config.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config.cfg b/config.cfg index 088ed012..168c359c 100644 --- a/config.cfg +++ b/config.cfg @@ -1,7 +1,10 @@ --- -# Add as many users as you want for your VPN server here. -# Credentials will be generated for each one. +# Add up to 250 users here. +# For each user, configuration files will be generated for both an IPsec +# connection and a WireGuard connection. Multiple client devices can share an +# IPsec configuration but WireGuard clients must each use a unique +# WireGuard configuration. users: - dan - jack From e478d31e50786acdba83bfa5ffa99c63b5d1410b Mon Sep 17 00:00:00 2001 From: David Myers Date: Fri, 7 Dec 2018 14:42:17 -0500 Subject: [PATCH 725/769] Update local install instructions (#1148) * Update local install instructions * Update deploy-to-ubuntu.md --- docs/deploy-to-ubuntu.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/docs/deploy-to-ubuntu.md b/docs/deploy-to-ubuntu.md index 6d6db62b..f3ba0669 100644 --- a/docs/deploy-to-ubuntu.md +++ b/docs/deploy-to-ubuntu.md @@ -1,20 +1,11 @@ # Local deployment -It is possible to download the Algo scripts to your own Ubuntu 18.04 server and run the scripts locally. +You can use Algo to configure a local server as an Algo VPN rather than create and configure a new server on a cloud provider. -In order to start, you need to install Ansible. Installing Ansible via pip requires pulling in a lot of dependencies, including a full compiler suite. It would be easier to use apt, however, Ubuntu 18.04 only comes with Ansible 2.0.0.2. The easiest solution is to install the Ansible PPA for a newer version of Ansible via apt, however, using a PPA requires installing `software-properties-common`. - -tl;dr: - -```shell -sudo apt-get install software-properties-common && sudo apt-add-repository ppa:ansible/ansible -sudo apt-get update && sudo apt-get install ansible python-pip build-essential python-dev libssl-dev libffi-dev -pip install virtualenv -pip install --upgrade pip -git clone https://github.com/trailofbits/algo -cd algo -python -m virtualenv env && source env/bin/activate && python -m pip install -U pip && python -m pip install -r requirements.txt -./algo +Install the Algo scripts on your server and follow the normal installation instructions, then choose: ``` +Install to existing Ubuntu 18.04 server (Advanced) +``` +Make sure your server is running the operating system specified. -**Warning**: Algo is intended to be run on a standalone server. If you run Algo on your existing server, the iptables rules will be overwritten. If you don't want to overwrite the rules, you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md). Other changes are also made, which can break other services running on your server (web, mail, etc.). +**PLEASE NOTE**: Algo is intended for use as a _dedicated_ VPN server. If you install Algo on an existing server, then any existing services might break. In particular, the firewall rules will be overwritten. If you don't want to overwrite the rules you must deploy via `ansible-playbook` and skip the `iptables` tag as described in [deploy-from-ansible.md](deploy-from-ansible.md), after which you'll need to implement the necessary rules yourself. From a4f2c97fd2f48c70e006e16e18e0609c1b16eeb4 Mon Sep 17 00:00:00 2001 From: "Federico G. Schwindt" Date: Mon, 10 Dec 2018 05:57:15 +0000 Subject: [PATCH 726/769] Fix ipv4 address missing on reboot (#1245) --- roles/common/tasks/freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index dda5dcf9..9f200189 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -42,7 +42,7 @@ block: | cloned_interfaces="lo100" ifconfig_lo100="inet {{ local_service_ip }} netmask 255.255.255.255" - ifconfig_lo100="inet6 FCAA::1/64" + ifconfig_lo100_ipv6="inet6 FCAA::1/64" notify: - restart loopback bsd tags: From a6cd89564ddc514636e0370e9d9cbf3e4f2e2321 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 10 Dec 2018 16:37:36 +0100 Subject: [PATCH 727/769] Fixes #1246 --- roles/cloud-vultr/tasks/prompts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/cloud-vultr/tasks/prompts.yml b/roles/cloud-vultr/tasks/prompts.yml index 84e0cfd9..69978e83 100644 --- a/roles/cloud-vultr/tasks/prompts.yml +++ b/roles/cloud-vultr/tasks/prompts.yml @@ -2,7 +2,7 @@ - pause: prompt: | Enter the local path to your configuration INI file - (https://github.com/trailofbits/algo/docs/cloud-vultr.md): + (https://trailofbits.github.io/algo/cloud-vultr.html): register: _vultr_config when: vultr_config is undefined From 955a986c219282acca5f617f0a311fef816732f1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 18 Dec 2018 13:59:25 +0100 Subject: [PATCH 728/769] IPv6 forwarding fixes (#1256) --- roles/common/tasks/freebsd.yml | 2 +- roles/common/tasks/main.yml | 1 + roles/common/tasks/ubuntu.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 9f200189..78f47397 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -21,7 +21,7 @@ sysctl: - item: net.inet.ip.forwarding value: 1 - - item: net.inet6.ip6.forwarding + - item: "{{ 'net.inet6.ip6.forwarding' if ipv6_support else none }}" value: 1 tags: - always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 73e6783f..21d51a46 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -15,6 +15,7 @@ - name: Sysctl tuning sysctl: name="{{ item.item }}" value="{{ item.value }}" + when: item.item != "" with_items: - "{{ sysctl|default([]) }}" tags: diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 9c6e6a5b..6dbc6335 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -114,7 +114,7 @@ value: 1 - item: net.ipv4.conf.all.forwarding value: 1 - - item: net.ipv6.conf.all.forwarding + - item: "{{ 'net.ipv6.conf.all.forwarding' if ipv6_support else none }}" value: 1 tags: - always From 0a098b31f96dcf2e01b5627fb16cbc027ebee097 Mon Sep 17 00:00:00 2001 From: Izzy Gomez Date: Thu, 20 Dec 2018 02:46:37 -0700 Subject: [PATCH 729/769] Fix typo in deploy-from-ansible.md. (#1261) --- docs/deploy-from-ansible.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 946c045b..f2809e0c 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -34,7 +34,7 @@ See below for more information about providers and extra variables - `ondemand_wifi_exclude` (Required if `ondemand_wifi` set) - WiFi networks to exclude from using the VPN. Comma-separated values - `local_dns` - (Optional) Enable a DNS resolver. Default: false - `ssh_tunneling` - (Optional) Enable SSH tunneling for each user. Default: false -- `windows` - (Optional) Enables compatible ciphers and key exchange to support Windows clietns, less secure. Default: false +- `windows` - (Optional) Enables compatible ciphers and key exchange to support Windows clients, less secure. Default: false - `store_cakey` - (Optional) Whether or not keep the CA key (required to add users in the future, but less secure). Default: false If any of those unspecified ansible will ask the user to input From f9702fb3df4538d22ed235a01868eb5a92619044 Mon Sep 17 00:00:00 2001 From: David Myers Date: Thu, 20 Dec 2018 04:47:24 -0500 Subject: [PATCH 730/769] Document DigitalOcean firewall (#1257) --- docs/cloud-do.md | 108 ++++++++++++++++++++++-------------- docs/images/do-firewall.png | Bin 0 -> 123960 bytes 2 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 docs/images/do-firewall.png diff --git a/docs/cloud-do.md b/docs/cloud-do.md index 675754a9..25d80a23 100644 --- a/docs/cloud-do.md +++ b/docs/cloud-do.md @@ -18,70 +18,94 @@ You will be returned to the **Tokens/Keys** tab, and your new key will be shown Copy or note down the hash that shows below the name you entered, as this will be necessary for the steps below. This value will disappear if you leave this page, and you'll need to regenerate it if you forget it. -## Using DigitalOcean with Algo (command) +## Using DigitalOcean with Algo (interactive) -These steps are for people who run Algo using Docker or using the "algo" command. +These steps are for those who run Algo using Docker or using the `./algo` command. -First you will be asked which server type to setup. You would want to enter "1" to use DigitalOcean. +Choose DigitalOcean as your provider: ``` - What provider would you like to use? +What provider would you like to use? 1. DigitalOcean 2. Amazon Lightsail 3. Amazon EC2 - 4. Microsoft Azure - 5. Google Compute Engine - 6. Scaleway - 7. OpenStack (DreamCompute optimised) - 8. Install to existing Ubuntu 18.04 server - + 4. Vultr + 5. Microsoft Azure + 6. Google Compute Engine + 7. Scaleway + 8. OpenStack (DreamCompute optimised) + 9. Install to existing Ubuntu 18.04 server (Advanced) + Enter the number of your desired provider -: 1 -``` - -Next you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (don't worry if don't see any output, as the key input is hidden by Algo). - -``` -Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): -[pasted values will not be displayed] : +1 ``` -You will be prompted for the server name to enter. Feel free to leave this as the default ("algo.local") if you are not certain how this will affect your setup. +Enter a name for your server. Leave this as the default if you are not certain how this will affect your setup: ``` Name the vpn server: -[algo.local]: +[algo]: ``` -After entering the server name the script ask which region you wish to setup your new Algo instance in. Enter the number next to name of the region. +After several prompts related to Algo features you will be asked for the API Token value. Paste the API Token value you copied when following the steps in [API Token creation](#api-token-creation) (you won't see any output as the key is not echoed by Algo): ``` - What region should the server be located in? - 1. Amsterdam (Datacenter 2) - 2. Amsterdam (Datacenter 3) - 3. Frankfurt - 4. London - 5. New York (Datacenter 1) - 6. New York (Datacenter 2) - 7. New York (Datacenter 3) - 8. San Francisco (Datacenter 1) - 9. San Francisco (Datacenter 2) - 10. Singapore - 11. Toronto - 12. Bangalore -Enter the number of your desired region: -[7]: 11 +Enter your API token. The token must have read and write permissions (https://cloud.digitalocean.com/settings/api/tokens): + (output is hidden): ``` -You will then be asked the remainder of the setup questions. +Finally you will be asked the region in which you wish to setup your new Algo server. This list is dynamic and can change based on availability of resources. Enter the number next to name of the region: -## Using DigitalOcean with Algo (via Ansible) +``` +What region should the server be located in? + 1. ams3 Amsterdam 3 + 2. blr1 Bangalore 1 + 3. fra1 Frankfurt 1 + 4. lon1 London 1 + 5. nyc1 New York 1 + 6. nyc3 New York 3 + 7. sfo2 San Francisco 2 + 8. sgp1 Singapore 1 + 9. tor1 Toronto 1 + +Enter the number of your desired region +[6] +: +9 +``` -If you are using Ansible to deploy to DigitalOcean, you will need to pass the API Token to Ansible as `do_token`. +## Using DigitalOcean with Algo (scripted) -For example, +If you are using Ansible directly to run Algo you will need to pass the API Token as `do_token`. For example: - ansible-playbook deploy.yml -e 'provider=digitalocean do_token=my_secret_token' +```shell +ansible-playbook main.yml -e "provider=digitalocean + server_name=algo + ondemand_cellular=true + ondemand_wifi=true + local_dns=false + ssh_tunneling=false + windows=false + store_cakey=true + region=nyc3 + do_token=token" +``` -Where "my_secret_token" is your API Token. For more references see [deploy-from-ansible](deploy-from-ansible.md) +For more, see [Scripted Deployment](deploy-from-ansible.md). + +## Using the DigitalOcean firewall with Algo + +Many cloud providers include the option to configure an external firewall between the Internet and your cloud server. For some providers this is mandatory and Algo will configure it for you, but for DigitalOcean the external firewall is optional. + +An Algo VPN runs its own firewall and doesn't require an external firewall, but you might wish to use the DigitalOcean firewall for example to limit the addresses which can connect to your Algo VPN over SSH, or perhaps to block SSH altogether. + +To configure the DigitalOcean firewall, go to **Networking**, **Firewalls**, and choose **Create Firewall**. + +Configure your **Inbound Rules** as follows: + +![Inbound Rules](/docs/images/do-firewall.png) + +Leave the **Outbound Rules** at their defaults. + +Under **Apply to Droplets** enter the tag `Environment:Algo` to apply this firewall to all current and future Algo VPNs you create. diff --git a/docs/images/do-firewall.png b/docs/images/do-firewall.png new file mode 100644 index 0000000000000000000000000000000000000000..26c9e5866cd2c35283b361805563fb2d3823d4e3 GIT binary patch literal 123960 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%0A*n}ri6OqTb_MHnCJr?*J*bfg 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 literal 0 HcmV?d00001 From 0177284fea3ae56b3ce7d852b8810eb7a3e3a90e Mon Sep 17 00:00:00 2001 From: Michael Schubert Date: Thu, 20 Dec 2018 15:20:39 +0100 Subject: [PATCH 731/769] README: fix small typos (#1262) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8737d5da..0e87e361 100644 --- a/README.md +++ b/README.md @@ -173,13 +173,13 @@ Note the admin username is `ubuntu` instead of `root` on providers other than Di ## Adding or Removing Users -If you chose the save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. +If you chose to save the CA certificate during the deploy process, then Algo's own scripts can easily add and remove users from the VPN server. 1. Update the `users` list in your `config.cfg` 2. Open a terminal, `cd` to the algo directory, and activate the virtual environment with `source env/bin/activate` 3. Run the command: `./algo update-users` -After this process completes, the Algo VPN server will contains only the users listed in the `config.cfg` file. +After this process completes, the Algo VPN server will contain only the users listed in the `config.cfg` file. ## Additional Documentation From 5981bb9cadefe0508c28db9ac40d7e4ae8bae477 Mon Sep 17 00:00:00 2001 From: David Myers Date: Thu, 20 Dec 2018 09:21:04 -0500 Subject: [PATCH 732/769] Replace 'max_mss' with 'reduce_mtu' (#1253) --- config.cfg | 17 +++-- docs/troubleshooting.md | 80 ++++++++++++++++++++---- roles/common/tasks/facts.yml | 5 ++ roles/vpn/templates/rules.v4.j2 | 4 +- roles/vpn/templates/rules.v6.j2 | 6 +- roles/wireguard/templates/client.conf.j2 | 2 + 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/config.cfg b/config.cfg index 168c359c..b0c7756d 100644 --- a/config.cfg +++ b/config.cfg @@ -24,15 +24,14 @@ vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true wireguard_port: 51820 -# MSS is the TCP Max Segment Size -# Setting the 'max_mss' Ansible variable can solve some issues related to packet fragmentation -# This appears to be necessary on (at least) Google Cloud, -# however, some routers also require a change to this parameter -# See also: -# - https://github.com/trailofbits/algo/issues/216 -# - https://github.com/trailofbits/algo/issues?utf8=%E2%9C%93&q=is%3Aissue%20mtu -# - https://serverfault.com/questions/601143/ssh-not-working-over-ipsec-tunnel-strongswan -#max_mss: 1316 +# Reduce the MTU of the VPN tunnel +# Some cloud and internet providers use a smaller MTU (Maximum Transmission +# Unit) than the normal value of 1500 and if you don't reduce the MTU of your +# VPN tunnel some network connections will hang. Algo will attempt to set this +# automatically based on your server, but if connections hang you might need to +# adjust this yourself. +# See: https://github.com/trailofbits/algo/blob/master/docs/troubleshooting.md#various-websites-appear-to-be-offline-through-the-vpn +reduce_mtu: 0 # StrongSwan log level # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index e9335947..fa0472f2 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -320,22 +320,76 @@ You're trying to connect Ubuntu or Debian to the Algo server through the Network ### Various websites appear to be offline through the VPN -This issue appears intermittently due to issues with MTU size. Different networks may require the MTU within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. +This issue appears occasionally due to issues with [MTU](https://en.wikipedia.org/wiki/Maximum_transmission_unit) size. Different networks may require the MTU to be within a specific range to correctly pass traffic. We made an effort to set the MTU to the most conservative, most compatible size by default but problems may still occur. -Advanced users can troubleshoot the correct MTU size by retrying `ping` with the "don't fragment" bit set, then decreasing packet size until it works. This will determine the correct MTU size for your network, which you then need to update on your network adapter. +If either your Internet service provider or your chosen cloud service provider use an MTU smaller than the normal value of 1500 you can use the `reduce_mtu` option in the file `config.cfg` to correspondingly reduce the size of the VPN tunnels created by Algo. Algo will attempt to automatically set `reduce_mtu` based on the MTU found on the server at the time of deployment, but it cannot detect if the MTU is smaller on the client side of the connection. -E.g., On Linux (client -- Ubuntu 18.04), connect to your IPsec tunnel then use the following commands to determine the correct MTU size: -``` -$ ping -M do -s 1500 www.google.com -PING www.google.com (74.125.22.147) 1500(1528) bytes of data. -ping: local error: Message too long, mtu=1438 -``` -Then, set the MTU size on your network adapter (wlan0 or eth0): -``` -$ sudo ifconfig wlan0 mtu 1438 -``` +If you change `reduce_mtu` you'll need to deploy a new Algo VPN. -You can also set the `max_mss` variable to a new value in config.cfg, and then redeploy your server rather than reconfigure the current one in-place. +To determine the value for `reduce_mtu` you should examine the MTU on your Algo VPN server's primary network interface (see below). You might algo want to run tests using `ping`, both on a local client *when not connected to the VPN* and also on your Algo VPN server (see below). Then take the smallest MTU you find (local or server side), subtract it from 1500, and use that for `reduce_mtu`. An exception to this is if you find the smallest MTU is your local MTU at 1492, typical for PPPoE connections, then no MTU reduction should be necessary. + +#### Check the MTU on the Algo VPN server + +To check the MTU on your server, SSH in to it, run the command `ifconfig`, and look for the MTU of the main network interface. For example: +``` +ens4: flags=4163 mtu 1460 +``` +The MTU shown here is 1460 instead of 1500. Therefore set `reduce_mtu: 40` in `config.cfg`. Algo should do this automatically. + +#### Determine the MTU using `ping` + +When using `ping` you increase the payload size with the "Don't Fragment" option set until it fails. The largest payload size that works, plus the `ping` overhead of 28, is the MTU of the connection. + +##### Example: Test on your Algo VPN server (Ubuntu) +``` +$ ping -4 -s 1432 -c 1 -M do github.com +PING github.com (192.30.253.112) 1432(1460) bytes of data. +1440 bytes from lb-192-30-253-112-iad.github.com (192.30.253.112): icmp_seq=1 ttl=53 time=13.1 ms + +--- github.com ping statistics --- +1 packets transmitted, 1 received, 0% packet loss, time 0ms +rtt min/avg/max/mdev = 13.135/13.135/13.135/0.000 ms + +$ ping -4 -s 1433 -c 1 -M do github.com +PING github.com (192.30.253.113) 1433(1461) bytes of data. +ping: local error: Message too long, mtu=1460 + +--- github.com ping statistics --- +1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms +``` +In this example the largest payload size that works is 1432. The `ping` overhead is 28 so the MTU is 1432 + 28 = 1460, which is 40 lower than the normal MTU of 1500. Therefore set `reduce_mtu: 40` in `config.cfg`. + +##### Example: Test on a macOS client *not connected to your Algo VPN* +``` +$ ping -c 1 -D -s 1464 github.com +PING github.com (192.30.253.113): 1464 data bytes +1472 bytes from 192.30.253.113: icmp_seq=0 ttl=50 time=169.606 ms + +--- github.com ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = 169.606/169.606/169.606/0.000 ms + +$ ping -c 1 -D -s 1465 github.com +PING github.com (192.30.253.113): 1465 data bytes + +--- github.com ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +``` +In this example the largest payload size that works is 1464. The `ping` overhead is 28 so the MTU is 1464 + 28 = 1492, which is typical for a PPPoE Internet connection and does not require an MTU adjustment. Therefore use the default of `reduce_mtu: 0` in `config.cfg`. + +#### Change the client MTU without redeploying the Algo VPN + +If you don't wish to deploy a new Algo VPN (which is required to incorporate a change to `reduce_mtu`) you can change the client side MTU of WireGuard clients and Linux IPsec clients without needing to make changes to your Algo VPN. + +For WireGuard on Linux, or macOS (when installed with `brew`), you can specify the MTU yourself in the client configuration file (typically `wg0.conf`). Refer to the documentation (see `man wg-quick`). + +For WireGuard on iOS and Android you can change the MTU in the app. + +For IPsec on Linux you can change the MTU of your network interface to match the required MTU. For example: +``` +sudo ifconfig eth0 mtu 1440 +``` +To make the change take affect after a reboot, on Ubuntu 18.04 and later edit the relevant file in the `/etc/netplan` directory (see `man netplan`). ### Clients appear stuck in a reconnection loop diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 29ee3f55..235e3ac9 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -28,3 +28,8 @@ set_fact: ipv6_support: "{% if ansible_default_ipv6['gateway'] is defined %}true{% else %}false{% endif %}" tags: always + +- name: Check size of MTU + set_fact: + reduce_mtu: "{% if reduce_mtu|int == 0 and ansible_default_ipv4['mtu']|int < 1500 %}{{ 1500 - ansible_default_ipv4['mtu']|int }}{% else %}{{ reduce_mtu|int }}{% endif %}" + tags: always diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/vpn/templates/rules.v4.j2 index 49c34e2f..1b487e6d 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/vpn/templates/rules.v4.j2 @@ -10,8 +10,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if max_mss is defined %} --A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% if reduce_mtu|int > 0 %} +-A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} {% endif %} COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/vpn/templates/rules.v6.j2 index a6d853f2..6095e211 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/vpn/templates/rules.v6.j2 @@ -10,10 +10,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if max_mss is defined %} -# MSS is the TCP Max Segment Size -# See rules.v4 for a more complete explanation --A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ max_mss }} +{% if reduce_mtu|int > 0 %} +-A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} {% endif %} COMMIT diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 05bdea00..2aa2b3de 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -2,6 +2,8 @@ PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} Address = {{ wireguard_client_ip }} DNS = {{ wireguard_dns_servers }} +{% if reduce_mtu|int > 0 %}MTU = {{ 1420 - reduce_mtu|int }} +{% endif %} [Peer] PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} From 5d74ded90fc8218e863ffd74085eaa8f7da4809a Mon Sep 17 00:00:00 2001 From: TC1977 <37350377+TC1977@users.noreply.github.com> Date: Wed, 2 Jan 2019 19:23:37 -0500 Subject: [PATCH 733/769] Update README.md (#1286) Adds Wireguard to the first line. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e87e361..1ea7d616 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40AlgoVPN)](https://twitter.com/AlgoVPN) [![TravisCI Status](https://api.travis-ci.org/trailofbits/algo.svg?branch=master)](https://travis-ci.org/trailofbits/algo) -Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. +Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC and Wireguard VPN. It uses the most secure defaults available, works with common cloud providers, and does not require client software on most devices. See our [release announcement](https://blog.trailofbits.com/2016/12/12/meet-algo-the-vpn-that-works/) for more information. ## Features From 9830947dfdc7ebc810a9e964ee1b0284719e0ea4 Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 2 Jan 2019 19:24:18 -0500 Subject: [PATCH 734/769] Sync list of supported cloud hosts (#1278) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea7d616..490bbb9d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC * Blocks ads with a local DNS resolver (optional) * Sets up limited SSH users for tunneling traffic (optional) * Based on current versions of Ubuntu and strongSwan -* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack or your own Ubuntu 18.04 LTS server +* Installs to DigitalOcean, Amazon Lightsail, Amazon EC2, Vultr, Microsoft Azure, Google Compute Engine, Scaleway, OpenStack, or your own Ubuntu 18.04 LTS server ## Anti-features @@ -29,7 +29,7 @@ Algo VPN is a set of Ansible scripts that simplify the setup of a personal IPSEC The easiest way to get an Algo server running is to let it set up a _new_ virtual machine in the cloud for you. -1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/) and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or an OpenStack based cloud hosting. +1. **Setup an account on a cloud hosting provider.** Algo supports [DigitalOcean](https://m.do.co/c/4d7f4ff9cfe4) (most user friendly), [Amazon Lightsail](https://aws.amazon.com/lightsail/), [Amazon EC2](https://aws.amazon.com/), [Vultr](https://www.vultr.com/), [Microsoft Azure](https://azure.microsoft.com/), [Google Compute Engine](https://cloud.google.com/compute/), [Scaleway](https://www.scaleway.com/), and [DreamCompute](https://www.dreamhost.com/cloud/computing/) or other OpenStack-based cloud hosting. 2. **[Download Algo](https://github.com/trailofbits/algo/archive/master.zip).** Unzip it in a convenient location on your local machine. From 44ab95f12b13523fff49960175a04864ac50f0d6 Mon Sep 17 00:00:00 2001 From: Angel Montes de Oca Date: Mon, 7 Jan 2019 23:48:05 -0800 Subject: [PATCH 735/769] Include Algo generated password (#1272) I change a line to Include the Algo generated password so the users do not need to manually enter the password when installing on Windows 10 computers. --- roles/vpn/templates/client_windows.ps1.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4ffce674..4a846f35 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -85,7 +85,7 @@ $CaCertificateBase64 = "{{ PayloadContentCA }}" $UserPkcs12Base64 = "{{ item.1.stdout }}" if ($PsCmdlet.ParameterSetName -eq "Add" -and -not $Pkcs12DecryptionPassword) { - $Pkcs12DecryptionPassword = Read-Host -AsSecureString -Prompt "Pkcs12DecryptionPassword" + $Pkcs12DecryptionPassword = ConvertTo-SecureString '{{ p12_export_password }}' -asplaintext -force } <# From 72763ddec4d8aa6a7afe859d6f40e5e034268401 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 8 Jan 2019 08:53:35 +0100 Subject: [PATCH 736/769] Update deploy-from-ansible.md --- docs/deploy-from-ansible.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index f2809e0c..9d3d3f29 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -179,7 +179,7 @@ Required variables: Required variables: -- [vultr_config](https://github.com/trailofbits/algo/docs/cloud-vultr.md) +- [vultr_config](https://trailofbits.github.io/algo/cloud-vultr.html) - [region](https://api.vultr.com/v1/regions/list) ### Azure From 11ed8b8f3034604378d95b9d50a39e94ebca1694 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 8 Jan 2019 08:57:40 +0100 Subject: [PATCH 737/769] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 490bbb9d..fa78fd23 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, #### Ubuntu Server 18.04 example -1. `sudo apt-get install strongswan strongswan-plugin-openssl`: install strongSwan +1. `sudo apt-get install strongswan libstrongswan-standard-plugins`: install strongSwan 2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` 3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//pki/private/.key` 4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//pki/cacert.pem` From 7a338b511dee334ace78a6e0a4f1c487516f7482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selim=20=C5=9Eumlu?= Date: Tue, 15 Jan 2019 06:23:48 +0300 Subject: [PATCH 738/769] Update deploy-from-windows.md (#1296) Updating the tutorial according to latest Windows 10 and Ubuntu changes --- docs/deploy-from-windows.md | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/deploy-from-windows.md b/docs/deploy-from-windows.md index 33fe1b92..62472b47 100644 --- a/docs/deploy-from-windows.md +++ b/docs/deploy-from-windows.md @@ -1,11 +1,8 @@ -# Windows client prerequisites +# Windows client prerequisite -Before run Algo, you have to have: +* 64-bit Windows 10 (Anniversary update or later version) -* Windows 10 (Anniversary update or later version) -* 64-bit installation (can't run on 32-bit systems) - -Once you verify your system is 64-bit and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': +Once you verify your system is 64-bit (32-bit is not supported) and up to date, you have to do a few manual steps to enable the 'Windows Subsystem for Linux': 1. Open 'Settings' 2. Click 'Update & Security', then click the 'For developers' option on the left. @@ -15,22 +12,16 @@ Wait a minute for Windows to install a few things in the background (it will eve 1. Click on 'Programs' 2. Click on 'Turn Windows features on or off' -3. Scroll down and check 'Windows Subsystem for Linux (Beta)', and then click OK. +3. Scroll down and check 'Windows Subsystem for Linux', and then click OK. +4. The subsystem will be installed, then Windows will require a restart. +5. Restart Windows and then [install Ubuntu from the Windows Store](https://www.microsoft.com/p/ubuntu/9nblggh4msv6). +6. Run Ubuntu from the Start menu. It will take a few minutes to install. It will have you create a separate user account for the Linux subsystem. Once that's done, you will finally have Ubuntu running somewhat integrated with Windows. -The subsystem will be installed, then Windows will require a reboot. Reboot, then open up the start menu and enter 'bash' (to open up 'Bash' installation in a new command prompt). Fill out all the questions (it will have you create a separate user account for the Linux subsystem), and once that's all done (it takes a few minutes to install), you will finally have Ubuntu running on your Windows laptop, somewhat integrated with Windows. Install additional packages: ```shell -sudo apt-get update && sudo apt-get install \ - git \ - build-essential \ - libssl-dev \ - libffi-dev \ - python-dev \ - python-pip \ - python-setuptools \ - python-virtualenv -y +sudo apt-get update && sudo apt-get install git build-essential libssl-dev libffi-dev python-dev python-pip python-setuptools python-virtualenv -y ``` Clone the Algo repository: From 7a6daff1ffdb92288311ed33dad24589f9edf92d Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sat, 19 Jan 2019 05:39:08 +0100 Subject: [PATCH 739/769] IPv6 fix (#1302) --- roles/common/tasks/freebsd.yml | 16 ++++++---------- roles/common/tasks/main.yml | 7 ++++--- roles/common/tasks/ubuntu.yml | 13 +++---------- users.yml | 1 + 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 78f47397..70ebe8fa 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,4 +1,10 @@ --- +- name: Gather facts + setup: + +- name: Gather additional facts + import_tasks: facts.yml + - set_fact: config_prefix: "/usr/local/" strongswan_shell: /usr/sbin/nologin @@ -23,17 +29,11 @@ value: 1 - item: "{{ 'net.inet6.ip6.forwarding' if ipv6_support else none }}" value: 1 - tags: - - always - -- setup: - name: Install tools package: name="{{ item }}" state=present with_items: - "{{ tools|default([]) }}" - tags: - - always - name: Loopback included into the rc config blockinfile: @@ -45,8 +45,6 @@ ifconfig_lo100_ipv6="inet6 FCAA::1/64" notify: - restart loopback bsd - tags: - - always - name: Enable the gateway features lineinfile: dest=/etc/rc.conf regexp='^{{ item.param }}.*' line='{{ item.param }}={{ item.value }}' @@ -59,8 +57,6 @@ - { param: natd_flags, value: '"-dynamic -m"' } notify: - restart ipfw - tags: - - always - name: FreeBSD | Activate IPFW shell: > diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index 21d51a46..a777eae6 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -6,12 +6,13 @@ - include_tasks: ubuntu.yml when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' + tags: + - update-users - include_tasks: freebsd.yml when: '"FreeBSD" in OS.stdout' - - - name: Gather additional facts - import_tasks: facts.yml + tags: + - update-users - name: Sysctl tuning sysctl: name="{{ item.item }}" value="{{ item.value }}" diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 6dbc6335..37d469e8 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -71,8 +71,6 @@ dest: /etc/systemd/network/10-algo-lo100.network notify: - restart systemd-networkd - tags: - - always - name: systemd services enabled and started systemd: @@ -83,12 +81,8 @@ with_items: - systemd-networkd - systemd-resolved - tags: - - always - meta: flush_handlers - tags: - - always - name: Check apparmor support shell: apparmor_status @@ -99,6 +93,9 @@ apparmor_enabled: true when: '"profiles are in enforce mode" in apparmor_status.stdout' +- name: Gather additional facts + import_tasks: facts.yml + - set_fact: tools: - git @@ -116,15 +113,11 @@ value: 1 - item: "{{ 'net.ipv6.conf.all.forwarding' if ipv6_support else none }}" value: 1 - tags: - - always - name: Install tools package: name="{{ item }}" state=present with_items: - "{{ tools|default([]) }}" - tags: - - always - name: Install headers apt: diff --git a/users.yml b/users.yml index 30e460ae..64422638 100644 --- a/users.yml +++ b/users.yml @@ -62,6 +62,7 @@ - block: - name: Local pre-tasks import_tasks: playbooks/cloud-pre.yml + become: false rescue: - debug: var=fail_hint tags: always From f25415dde345cf7e77a5333d9c54db0230d42562 Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 23 Jan 2019 01:12:43 -0500 Subject: [PATCH 740/769] Document using WireGuard on iOS (#1266) --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa78fd23..1a14dafb 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,27 @@ Certificates and configuration files that users will need are placed in the `con ### Apple Devices -**Send users their Apple Profile.** Find the corresponding mobileconfig (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. +Apple devices can connect to an Algo VPN via IPsec using their built-in IPsec support or via WireGuard by installing WireGuard client software. -**Turn on the VPN.** On iOS, connect to the VPN by opening Settings and clicking the toggle next to "VPN" near the top of the list. On macOS, connect to the VPN by opening System Preferences -> Network, finding Algo VPN in the left column and clicking "Connect." On macOS, check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. +#### Install WireGuard -**Managing On-Demand VPNs.** If you enabled "On Demand", the VPN will connect automatically whenever it is able. On iOS, you can turn off "On Demand" by clicking the (i) next to the entry for Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "On Demand" by opening the Network Preferences, finding Algo VPN in the left column, and unchecking the box for "Connect on demand." +On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the App Store. For each user you defined, Algo generated a WireGuard configuration file `wireguard/.conf` and a corresponding QR code image `wireguard/.png`. Either AirDrop the configuration file to the iOS device or use the WireGuard app to scan the QR code. To use "Connect On Demand" with WireGuard enable it by editing the configuration in the WireGuard app. + +Until the WireGuard app for macOS is ready, installing WireGuard on macOS is a little more complicated. See [Using MacOS as a Client with WireGuard](docs/client-macos-wireguard.md). + +#### Configure IPsec + +Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. + +#### Enable the VPN + +On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. + +#### Managing "Connect On Demand" + +If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". + +On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. ### Android Devices From b8e1c253c63662eddb2118d9aa5ed43ed6ca0c52 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 23 Jan 2019 07:14:37 +0100 Subject: [PATCH 741/769] Fixes #1305 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1a14dafb..45b4f4ab 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,6 @@ Depending on the platform, you may need one or multiple of the following files. * cacert.pem: CA Certificate * user.mobileconfig: Apple Profile * user.p12: User Certificate and Private Key (in PKCS#12 format) -* user.sswan: Android strongSwan Profile * ipsec_user.conf: strongSwan client configuration * ipsec_user.secrets: strongSwan client configuration * windows_user.ps1: Powershell script to help setup a VPN connection on Windows From b89d406ee004405865c77604b4f46b492b5f0c6a Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 24 Jan 2019 13:11:34 +0100 Subject: [PATCH 742/769] Update deploy-from-ansible.md (#1307) --- docs/deploy-from-ansible.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 9d3d3f29..361272d3 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -202,6 +202,28 @@ Required variables: Possible options can be gathered via cli `aws lightsail get-regions` +#### Minimum required IAM permissions for deployment: + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LightsailDeployment", + "Effect": "Allow", + "Action": [ + "lightsail:GetInstance", + "lightsail:CreateInstances", + "lightsail:OpenInstancePublicPorts" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + ### Scaleway Required variables: From 6233642c66f9310f2d95c71ec3e11258ccae6701 Mon Sep 17 00:00:00 2001 From: Luvpreet Singh Date: Sat, 26 Jan 2019 03:06:44 +0530 Subject: [PATCH 743/769] fix(update-users): changed generate p12 password task (#1289) Changed task's module to generic python format for python2 and python3. --- roles/common/tasks/facts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/tasks/facts.yml b/roles/common/tasks/facts.yml index 235e3ac9..6e79bfce 100644 --- a/roles/common/tasks/facts.yml +++ b/roles/common/tasks/facts.yml @@ -9,7 +9,7 @@ - name: Generate p12 export password local_action: module: shell - openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print "".join([chars[ord(c) % 64] for c in list(sys.stdin.read())])' + openssl rand 8 | python -c 'import sys,string; chars=string.ascii_letters + string.digits + "_@"; print("".join([chars[ord(c) % 64] for c in list(sys.stdin.read())]))' register: p12_password_generated when: p12_password is not defined tags: update-users From f1cb183ecfd5d64c75357ae9db60ec3b7eac51e1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov Date: Mon, 28 Jan 2019 13:42:11 +0100 Subject: [PATCH 744/769] Travis-CI fixes --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6fa1d0fc..14036294 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ RUN apk --no-cache add ${BUILD_PACKAGES} && \ python -m pip --no-cache-dir install virtualenv && \ python -m virtualenv env && \ source env/bin/activate && \ - python -m pip --no-cache-dir install -r requirements.txt && \ + python -m pip --no-cache-dir install -r requirements.txt --no-use-pep51 && \ apk del ${BUILD_PACKAGES} COPY . . RUN chmod 0755 /algo/algo-docker.sh From a266b4d633493d4766e1cce6d0689cb755a0881d Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 28 Jan 2019 23:50:58 +0100 Subject: [PATCH 745/769] Allow windows users install VPN for all users in the system (#1310) --- docs/client-windows.md | 7 +++++++ roles/vpn/templates/client_windows.ps1.j2 | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/docs/client-windows.md b/docs/client-windows.md index 53b62f22..323da8df 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -10,6 +10,13 @@ To install automatically, use the generated user Powershell script. ```powershell powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add ``` + +If you have more than one account on your Windows 10 machine (e.g. one with administrator privileges and one without) and would like to have the VPN connection available to all users, pass the parameter `-AllUsers` + +```powershell +powershell -ExecutionPolicy ByPass -File C:\path\to\windows_USER.ps1 -Add -AllUsers +``` + 4. The command has help information available. To view its full help, run this from Powershell: ```powershell Get-Help -Name .\windows_USER.ps1 -Full | more diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/vpn/templates/client_windows.ps1.j2 index 4a846f35..e1021bbe 100644 --- a/roles/vpn/templates/client_windows.ps1.j2 +++ b/roles/vpn/templates/client_windows.ps1.j2 @@ -29,6 +29,9 @@ Note that this must be passed in as a SecureString, not a regular string. You can create a secure string with the `Read-Host -AsSecureString` cmdlet. See the examples for more information. +.PARAMETER AllUsers +Allow all users to use the VPN + .EXAMPLE client_USER.ps1 -Add @@ -63,6 +66,9 @@ Save the embedded CA cert and encrypted user PKCS12 file. [Parameter(ParameterSetName="Add")] [SecureString] $Pkcs12DecryptionPassword, + [Parameter(ParameterSetName="Add")] + [Switch] $AllUsers = $false, + [Parameter(Mandatory, ParameterSetName="Remove")] [Switch] $Remove, @@ -164,6 +170,7 @@ function Add-AlgoVPN { TunnelType = "IKEv2" AuthenticationMethod = "MachineCertificate" EncryptionLevel = "Required" + AllUserConnection = $AllUsers } Add-VpnConnection @addVpnParams From c47dd4a7abc401388b8165989a2f6ef3ca7b88c4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 28 Jan 2019 23:51:28 +0100 Subject: [PATCH 746/769] encode wifi networks to base64 (#1303) --- roles/vpn/templates/mobileconfig.j2 | 4 ++-- server.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index 8a0bb5f6..b48500c2 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -12,8 +12,8 @@ 1 OnDemandRules -{% if algo_ondemand_wifi_exclude != '_null' %} -{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|string).split(',') %} +{% if algo_ondemand_wifi_exclude|b64decode != '_null' %} +{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} Action Disconnect diff --git a/server.yml b/server.yml index b6e8340b..f643f4f8 100644 --- a/server.yml +++ b/server.yml @@ -49,7 +49,7 @@ algo_server_name: {{ algo_server_name }} algo_ondemand_cellular: {{ algo_ondemand_cellular }} algo_ondemand_wifi: {{ algo_ondemand_wifi }} - algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude }} + algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude | b64encode }} algo_local_dns: {{ algo_local_dns }} algo_ssh_tunneling: {{ algo_ssh_tunneling }} algo_windows: {{ algo_windows }} From 43ed5b2aaae0814039a40c7e6014df68a312d516 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 30 Jan 2019 07:23:11 +0100 Subject: [PATCH 747/769] add flags=(attach_disconnected) to dnscrypt-proxy apparmor profile (#1312) --- roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy index 7e900bc5..c2258688 100644 --- a/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy +++ b/roles/dns_encryption/files/apparmor.profile.dnscrypt-proxy @@ -1,6 +1,6 @@ #include -/usr/bin/dnscrypt-proxy { +/usr/bin/dnscrypt-proxy flags=(attach_disconnected) { #include #include #include From 4a6888add65d4ffaec5cedfd16c5ba7692c1eb79 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 4 Feb 2019 16:04:30 +0100 Subject: [PATCH 748/769] WiFi exclude list fix (#1318) --- input.yml | 8 ++++---- server.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/input.yml b/input.yml index f24ab2ba..b6e5e81c 100644 --- a/input.yml +++ b/input.yml @@ -116,10 +116,10 @@ {% if ondemand_wifi is defined %}{{ ondemand_wifi | bool }} {%- elif _ondemand_wifi.user_input is defined and _ondemand_wifi.user_input != "" %}{{ booleans_map[_ondemand_wifi.user_input] | default(defaults['ondemand_wifi']) }} {%- else %}false{% endif %} - algo_ondemand_wifi_exclude: >- - {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude }} - {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input }} - {%- else %}_null{% endif %} + algo_ondemand_wifi_exclude: >- + {% if ondemand_wifi_exclude is defined %}{{ ondemand_wifi_exclude | b64encode }} + {%- elif _ondemand_wifi_exclude.user_input is defined and _ondemand_wifi_exclude.user_input != "" %}{{ _ondemand_wifi_exclude.user_input | b64encode }} + {%- else %}{{ '_null' | b64encode }}{% endif %} algo_local_dns: >- {% if local_dns is defined %}{{ local_dns | bool }} {%- elif _local_dns.user_input is defined and _local_dns.user_input != "" %}{{ booleans_map[_local_dns.user_input] | default(defaults['local_dns']) }} diff --git a/server.yml b/server.yml index f643f4f8..b6e8340b 100644 --- a/server.yml +++ b/server.yml @@ -49,7 +49,7 @@ algo_server_name: {{ algo_server_name }} algo_ondemand_cellular: {{ algo_ondemand_cellular }} algo_ondemand_wifi: {{ algo_ondemand_wifi }} - algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude | b64encode }} + algo_ondemand_wifi_exclude: {{ algo_ondemand_wifi_exclude }} algo_local_dns: {{ algo_local_dns }} algo_ssh_tunneling: {{ algo_ssh_tunneling }} algo_windows: {{ algo_windows }} From 9f66e4760763bd3db50e3718f3caeb8f009ca0b4 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 7 Feb 2019 15:09:09 +0100 Subject: [PATCH 749/769] Closes #1321 --- docs/deploy-from-ansible.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index 361272d3..ccbb05e2 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -212,6 +212,7 @@ Possible options can be gathered via cli `aws lightsail get-regions` "Sid": "LightsailDeployment", "Effect": "Allow", "Action": [ + "lightsail:GetRegions", "lightsail:GetInstance", "lightsail:CreateInstances", "lightsail:OpenInstancePublicPorts" From 40b42c4f337b5c89b9ce8122c54ce2f8cfee706b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 8 Feb 2019 13:34:01 +0100 Subject: [PATCH 750/769] Get started with Azure more easily (#1323) --- docs/cloud-azure.md | 102 ++++++++++++++-------------- roles/cloud-azure/tasks/prompts.yml | 48 ++----------- 2 files changed, 56 insertions(+), 94 deletions(-) diff --git a/docs/cloud-azure.md b/docs/cloud-azure.md index 261f4bcf..22239d6b 100644 --- a/docs/cloud-azure.md +++ b/docs/cloud-azure.md @@ -1,58 +1,60 @@ # Azure cloud setup -| Instruction | Screenshot(s) | -| ----------- | ---------- | -| 1. Go to https://portal.azure.com/ | | -| 2. Go to **Azure Active Directory** | [![step2-thumb]][step2-screen] | -| 3. Go to **App registrations** and click to **Add** | [![step3-thumb]][step3-screen] | -| 4. Fill out the forms and click **Create** | [![step4-thumb]][step4-screen] | -| 5. Click on the app name | [![step5-thumb]][step5-screen] | -| 6. Copy and save somewhere the **Application ID** and click on **Keys**. | [![step6-thumb]][step6-screen] | -| 7. Fill out the forms and click **Save**. Copy and save somewhere the **Secret ID** (the value) | [![step7-thumb]][step7-screen] | -| 8. Go to the **Main menu**, **Azure Active Directory** and click on **Properties**. Copy and save somewhere the **Directory ID** | [![step8-thumb]][step8-screen] | -| 9. Go to the **Main menu**, **Subscriptions** and click on the subscription you want you use in Algo. Copy and save the subscription id from the **Overview** tab | [![step9-thumb]][step9-screen] | -| 10. Go to the **Access control (IAM)** tab and click to **Add** | [![step10-thumb]][step10-screen] | -| 11. Select a role (Contributor will be sufficient)| [![step11-thumb]][step11-screen] | -| 12. Next, switch to **Add users** and search by the **App name** (the name from the 4th step) and select it. | [![step12-thumb]][step12-screen] | +The easiest way to get started with the Azure CLI is by running it in an Azure Cloud Shell environment through your browser. -Now you can use Environment Variables: +Here you can find some information from [the official doc](https://docs.microsoft.com/en-us/cli/azure/get-started-with-azure-cli?view=azure-cli-latest). We put the essential commands together for simplest usage. -* AZURE_CLIENT_ID - from the 6th step -* AZURE_SECRET - from the 7th step -* AZURE_TENANT - from the 8th step -* AZURE_SUBSCRIPTION_ID - from the 9th step +## Install azure-cli -or create the credentials file ``~/.azure/credentials`: +- macOS ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-macos?view=azure-cli-latest)): + ```bash + $ brew update && brew install azure-cli + ``` -``` -[default] -client_id= -secret= -tenant= -subscription_id= -``` -or just pass those values to the Algo script +- Linux (deb-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest)): + ```bash + $ sudo apt-get update && sudo apt-get install \ + apt-transport-https \ + lsb-release \ + software-properties-common \ + dirmngr -y + $ AZ_REPO=$(lsb_release -cs) + $ echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | \ + sudo tee /etc/apt/sources.list.d/azure-cli.list + $ sudo apt-key --keyring /etc/apt/trusted.gpg.d/Microsoft.gpg adv \ + --keyserver packages.microsoft.com \ + --recv-keys BC528686B50D79E339D3721CEB3E94ADBE1229CF + $ sudo apt-get update + $ sudo apt-get install azure-cli + ``` -[step2-screen]: http://i.imgur.com/ENvSupE.png -[step3-screen]: http://i.imgur.com/sPLQaQe.jpg -[step4-screen]: http://i.imgur.com/di3xFCM.jpg -[step5-screen]: http://i.imgur.com/SipQyRA.jpg -[step6-screen]: http://i.imgur.com/RRTqV7C.jpg -[step7-screen]: http://i.imgur.com/ZnqJeVv.jpg -[step8-screen]: http://i.imgur.com/WAS8Ovl.png -[step9-screen]: http://i.imgur.com/IvTN7o1.jpg -[step10-screen]: http://i.imgur.com/j6dgo75.png -[step11-screen]: http://i.imgur.com/NUJ6k7i.jpg -[step12-screen]: http://i.imgur.com/VZv5qwb.jpg +- Linux (rpm-based) ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-yum?view=azure-cli-latest)): + ```bash + $ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc + $ sudo sh -c 'echo -e "[azure-cli]\nname=Azure CLI\nbaseurl=https://packages.microsoft.com/yumrepos/azure-cli\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" > /etc/yum.repos.d/azure-cli.repo' + $ sudo yum install azure-cli + ``` -[step2-thumb]: https://i.imgur.com/ENvSupEm.png -[step3-thumb]: https://i.imgur.com/sPLQaQem.jpg -[step4-thumb]: https://i.imgur.com/di3xFCMm.jpg -[step5-thumb]: https://i.imgur.com/SipQyRAm.jpg -[step6-thumb]: https://i.imgur.com/RRTqV7Cm.jpg -[step7-thumb]: https://i.imgur.com/ZnqJeVvm.jpg -[step8-thumb]: https://i.imgur.com/WAS8Ovlm.png -[step9-thumb]: https://i.imgur.com/IvTN7o1m.jpg -[step10-thumb]: https://i.imgur.com/j6dgo75m.png -[step11-thumb]: https://i.imgur.com/NUJ6k7im.jpg -[step12-thumb]: https://i.imgur.com/VZv5qwbm.jpg +- Windows ([link](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest)): + For Windows the Azure CLI is installed via an MSI, which gives you access to the CLI through the Windows Command Prompt (CMD) or PowerShell. When installing for Windows Subsystem for Linux (WSL), packages are available for your Linux distribution. [Download the MSI installer](https://aka.ms/installazurecliwindows) + +If your OS is missing or to get more information see [the official doc](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) + + +## Sign in + +1. Run the `login` command: +```bash +az login +``` + + If the CLI can open your default browser, it will do so and load a sign-in page. + + Otherwise, you need to open a browser page and follow the instructions on the command line to enter an authorization code after navigating to https://aka.ms/devicelogin in your browser. + +2. Sign in with your account credentials in the browser. + +There are ways to sign in non-interactively, which are covered in detail in [Sign in with Azure CLI](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli?view=azure-cli-latest). + + +**Now you are able to deploy an AlgoVPN instance without hassle** diff --git a/roles/cloud-azure/tasks/prompts.yml b/roles/cloud-azure/tasks/prompts.yml index 28d42521..09717205 100644 --- a/roles/cloud-azure/tasks/prompts.yml +++ b/roles/cloud-azure/tasks/prompts.yml @@ -1,49 +1,9 @@ --- -- pause: - prompt: | - Enter your azure secret id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_secret - when: - - azure_secret is undefined - - lookup('env','AZURE_SECRET')|length <= 0 - -- pause: - prompt: | - Enter your azure tenant id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_tenant - when: - - azure_tenant is undefined - - lookup('env','AZURE_TENANT')|length <= 0 - -- pause: - prompt: | - Enter your azure client id (application id) (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_client_id - when: - - azure_client_id is undefined - - lookup('env','AZURE_CLIENT_ID')|length <= 0 - -- pause: - prompt: | - Enter your azure subscription id (https://github.com/trailofbits/algo/blob/master/docs/cloud-azure.md) - You can skip this step if you want to use your defaults credentials from ~/.azure/credentials - echo: false - register: _azure_subscription_id - when: - - azure_subscription_id is undefined - - lookup('env','AZURE_SUBSCRIPTION_ID')|length <= 0 - - set_fact: - secret: "{{ azure_secret | default(_azure_secret.user_input|default(None)) | default(lookup('env','AZURE_SECRET'), true) }}" - tenant: "{{ azure_tenant | default(_azure_tenant.user_input|default(None)) | default(lookup('env','AZURE_TENANT'), true) }}" - client_id: "{{ azure_client_id | default(_azure_client_id.user_input|default(None)) | default(lookup('env','AZURE_CLIENT_ID'), true) }}" - subscription_id: "{{ azure_subscription_id | default(_azure_subscription_id.user_input|default(None)) | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" + secret: "{{ azure_secret | default(lookup('env','AZURE_SECRET'), true) }}" + tenant: "{{ azure_tenant | default(lookup('env','AZURE_TENANT'), true) }}" + client_id: "{{ azure_client_id | default(lookup('env','AZURE_CLIENT_ID'), true) }}" + subscription_id: "{{ azure_subscription_id | default(lookup('env','AZURE_SUBSCRIPTION_ID'), true) }}" - block: - name: Set facts about the regions From 1be0908c5162b6d1a06e97db6c4ec528dfed96f7 Mon Sep 17 00:00:00 2001 From: David Myers Date: Tue, 12 Feb 2019 05:19:38 -0500 Subject: [PATCH 751/769] Add note about new WireGuard for iOS default MTU (#1293) --- docs/troubleshooting.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index fa0472f2..493661cf 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -391,6 +391,10 @@ sudo ifconfig eth0 mtu 1440 ``` To make the change take affect after a reboot, on Ubuntu 18.04 and later edit the relevant file in the `/etc/netplan` directory (see `man netplan`). +#### Note for WireGuard iOS users + +As of WireGuard for iOS 0.0.20190107 the default MTU is 1280, a conservative value intended to allow mobile devices to continue to work as they switch between different networks which might have smaller than normal MTUs. In order to use this default MTU review the configuration in the WireGuard app and remove any value for MTU that might have been added automatically by Algo. + ### Clients appear stuck in a reconnection loop If you're using 'Connect on Demand' on iOS and your client device appears stuck in a reconnection loop after switching from WiFi to LTE or vice versa, you may want to try disabling DoS protection in strongSwan. From df3d547fb3ec939818ec91fa907aa0e1d2aa9343 Mon Sep 17 00:00:00 2001 From: David Myers Date: Sun, 17 Feb 2019 18:38:19 -0500 Subject: [PATCH 752/769] Document using WireGuard app on macOS (#1327) * Document using WireGuard app on macOS * Update README.md * Make WireGuard the default for Apple devices * clarify user list * fix tests * connect on demand --- README.md | 23 ++++++----------------- config.cfg | 15 +++++++-------- docs/client-apple-ipsec.md | 15 +++++++++++++++ docs/client-macos-wireguard.md | 19 +++++++++++-------- tests/update-users.sh | 2 +- 5 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 docs/client-apple-ipsec.md diff --git a/README.md b/README.md index 45b4f4ab..be4ce6be 100644 --- a/README.md +++ b/README.md @@ -89,27 +89,15 @@ Certificates and configuration files that users will need are placed in the `con ### Apple Devices -Apple devices can connect to an Algo VPN via IPsec using their built-in IPsec support or via WireGuard by installing WireGuard client software. +WireGuard is used to provide VPN services on Apple devices. Algo generates a WireGuard configuration file, `wireguard/.conf`, and a QR code, `wireguard/.png`, for each user defined in `config.cfg`. -#### Install WireGuard +On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the iOS App Store. Then, use the WireGuard app to scan the QR code or AirDrop the configuration file to the device. -On iOS, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1441195209?mt=8) app from the App Store. For each user you defined, Algo generated a WireGuard configuration file `wireguard/.conf` and a corresponding QR code image `wireguard/.png`. Either AirDrop the configuration file to the iOS device or use the WireGuard app to scan the QR code. To use "Connect On Demand" with WireGuard enable it by editing the configuration in the WireGuard app. +On macOS Mojave or later, install the [WireGuard](https://itunes.apple.com/us/app/wireguard/id1451685025?mt=12) app from the Mac App Store. WireGuard will appear in the menu bar once you run the app. Click on the WireGuard icon, choose **Import tunnel(s) from file...**, then select the appropriate WireGuard configuration file. Enable "Connect on Demand" by editing the tunnel configuration in the WireGuard app. -Until the WireGuard app for macOS is ready, installing WireGuard on macOS is a little more complicated. See [Using MacOS as a Client with WireGuard](docs/client-macos-wireguard.md). +Installing WireGuard is a little more complicated on older version of macOS. See [Using macOS as a Client with WireGuard](docs/client-macos-wireguard.md). -#### Configure IPsec - -Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. - -#### Enable the VPN - -On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. - -#### Managing "Connect On Demand" - -If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". - -On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. +If you prefer to use the built-in IPSEC VPN on Apple devices, then see [Using Apple Devices as a Client with IPSEC](docs/client-apple-ipsec.md). ### Android Devices @@ -208,6 +196,7 @@ After this process completes, the Algo VPN server will contain only the users li - Setup [Android](docs/client-android.md) clients - Setup [Generic/Linux](docs/client-linux.md) clients with Ansible - Setup Ubuntu clients to use [WireGuard](docs/client-linux-wireguard.md) + - Setup Apple devices to use [IPSEC](docs/client-apple-ipsec.md) * Cloud setup - Configure [Amazon EC2](docs/cloud-amazon-ec2.md) - Configure [Azure](docs/cloud-azure.md) diff --git a/config.cfg b/config.cfg index b0c7756d..3f5bdcb9 100644 --- a/config.cfg +++ b/config.cfg @@ -1,15 +1,14 @@ --- -# Add up to 250 users here. -# For each user, configuration files will be generated for both an IPsec -# connection and a WireGuard connection. Multiple client devices can share an -# IPsec configuration but WireGuard clients must each use a unique -# WireGuard configuration. +# This is the list of user to generate. +# Every device must have a unique username. +# You can generate up to 250 users at one time. users: - - dan - - jack + - phone + - laptop + - desktop -# NOTE: If your usernames have leading 0's, like "000dan", you have to escape them +# NOTE: You must "escape" any usernames with leading 0's, like "000dan" ### Advanced users only below this line ### diff --git a/docs/client-apple-ipsec.md b/docs/client-apple-ipsec.md new file mode 100644 index 00000000..e740b231 --- /dev/null +++ b/docs/client-apple-ipsec.md @@ -0,0 +1,15 @@ +# Using the built-in IPSEC VPN on Apple Devices + +## Configure IPsec + +Find the corresponding `mobileconfig` (Apple Profile) for each user and send it to them over AirDrop or other secure means. Apple Configuration Profiles are all-in-one configuration files for iOS and macOS devices. On macOS, double-clicking a profile to install it will fully configure the VPN. On iOS, users are prompted to install the profile as soon as the AirDrop is accepted. + +## Enable the VPN + +On iOS, connect to the VPN by opening **Settings** and clicking the toggle next to "VPN" near the top of the list. If using WireGuard you can also enable the VPN from the WireGuard app. On macOS, connect to the VPN by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, and clicking "Connect." Check "Show VPN status in menu bar" to easily connect and disconnect from the menu bar. + +## Managing "Connect On Demand" + +If you enabled "Connect On Demand" the VPN will connect automatically whenever it is able. Most Apple users will want to enable "Connect On Demand", but if you do then simply disabling the VPN will not cause it to stay disabled; it will just "Connect On Demand" again. To disable the VPN you'll need to disable "Connect On Demand". + +On iOS, you can turn off "Connect On Demand" in **Settings** by clicking the (i) next to the entry for your Algo VPN and toggling off "Connect On Demand." On macOS, you can turn off "Connect On Demand" by opening **System Preferences** -> **Network**, finding the Algo VPN in the left column, unchecking the box for "Connect on demand", and clicking Apply. \ No newline at end of file diff --git a/docs/client-macos-wireguard.md b/docs/client-macos-wireguard.md index 0d1db781..cce6ccc4 100644 --- a/docs/client-macos-wireguard.md +++ b/docs/client-macos-wireguard.md @@ -1,31 +1,34 @@ -# Using MacOS as a Client with WireGuard +# MacOS WireGuard Client Setup + +The WireGuard macOS app is unavailable for older operating systems. Please update your operating system if you can. If you are on a macOS High Sierra (10.13) or earlier, then you can still use WireGuard via their userspace drivers via the process detailed below. ## Install WireGuard -To connect to your Algo VPN using [WireGuard](https://www.wireguard.com) from MacOS +Install the wireguard-go userspace driver: ``` -# Install the wireguard-go userspace driver brew install wireguard-tools ``` ## Locate the Config File -The Algo-generated config files for WireGuard are named `configs//wireguard/.conf` on the system where you ran `./algo`. One file was generated for each of the users you added to `config.cfg` before you ran `./algo`. Each Linux and Android client you connect to your Algo VPN must use a different WireGuard config file. Choose one of these files and copy it to your device. +Algo generates a WireGuard configuration file, `wireguard/.conf`, and a QR code, `wireguard/.png`, for each user defined in `config.cfg`. Find the configuration file and copy it to your device if you don't already have it. + +Note that each client you use to connect to Algo VPN must have a unique WireGuard config. ## Configure WireGuard -Finally, install the config file on your client as `/usr/local/etc/wireguard/wg0.conf` and start WireGuard: +You'll need to copy the appropriate WireGuard configuration file into a location where the userspace driver can find it. After it is in the right place, start the VPN, and verify connectivity. ``` -# Install the config file to the WireGuard configuration directory on your MacOS device +# Copy the config file to the WireGuard configuration directory on your macOS device mkdir /usr/local/etc/wireguard/ cp .conf /usr/local/etc/wireguard/wg0.conf -# Start the WireGuard VPN: +# Start the WireGuard VPN sudo wg-quick up wg0 -# Verify the connection to the Algo VPN: +# Verify the connection to the Algo VPN wg # See that your client is using the IP address of your Algo VPN: diff --git a/tests/update-users.sh b/tests/update-users.sh index ba40bb33..2387103f 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -11,7 +11,7 @@ else ansible-playbook users.yml -e "${USER_ARGS}" -t update-users fi -if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/jack.crt | grep CRL +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.crt | grep CRL then echo "The CRL check passed" else From 5cb1fdd3399238ffc0965f4d2fda0d3b9ba2b2bd Mon Sep 17 00:00:00 2001 From: David Myers Date: Wed, 20 Feb 2019 10:08:25 -0500 Subject: [PATCH 753/769] Clarify prompts (#1331) --- input.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/input.yml b/input.yml index b6e5e81c..34ae4f66 100644 --- a/input.yml +++ b/input.yml @@ -51,21 +51,21 @@ - pause: prompt: | - Do you want macOS/iOS clients to enable "VPN On Demand" when connected to cellular networks? + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? [y/N] register: _ondemand_cellular when: ondemand_cellular is undefined - pause: prompt: | - Do you want macOS/iOS clients to enable "VPN On Demand" when connected to Wi-Fi? + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? [y/N] register: _ondemand_wifi when: ondemand_wifi is undefined - pause: prompt: | - List the names of trusted Wi-Fi networks (if any) that macOS/iOS clients exclude from using the VPN + List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) register: _ondemand_wifi_exclude when: @@ -75,7 +75,7 @@ - pause: prompt: | - Do you want to install a DNS resolver on this VPN server, to block ads while surfing? + Do you want to install an ad blocking DNS resolver on this VPN server? [y/N] register: _local_dns when: local_dns is undefined From bfe168d31cf52d9c9f4277924a35e033ab8840a8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 22 Feb 2019 16:00:47 +0100 Subject: [PATCH 754/769] Closes #1059 --- docs/client-windows.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/client-windows.md b/docs/client-windows.md index 323da8df..62d73cf2 100644 --- a/docs/client-windows.md +++ b/docs/client-windows.md @@ -29,7 +29,7 @@ Get-Help -Name .\windows_USER.ps1 -Full | more 3. If you haven't already, you will need to change the Execution Policy to allow unsigned scripts to run. ```powershell -Set-ExecutionPolicy Unrestricted -Scope CurrentUser +Set-ExecutionPolicy Unrestricted -Scope Process ``` 4. In the same window, run the necessary commands to install the certificates and create the VPN configuration. Note the lines at the top defining the VPN address, USER.p12 file location, and CA certificate location - change those lines to the IP address of your Algo server and the location you saved those two files. Also note that it will prompt for the "User p12 password", which is printed at the end of a successful Algo deployment. @@ -69,10 +69,4 @@ Set-VpnConnectionIPsecConfiguration @setVpnParams ``` -5. After you execute the user script, set the Execution Policy back before you close the PowerShell window. - -```powershell -Set-ExecutionPolicy Restricted -Scope CurrentUser -``` - Your VPN is now installed and ready to use. From 216cd09dcfae48937fab0f02c567cdca75b18248 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 25 Feb 2019 17:56:19 +0100 Subject: [PATCH 755/769] Disable wireguard PersistentKeepalive by default (#1338) --- config.cfg | 8 ++++++-- roles/wireguard/defaults/main.yml | 1 + roles/wireguard/templates/client.conf.j2 | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/config.cfg b/config.cfg index 3f5bdcb9..15aa586e 100644 --- a/config.cfg +++ b/config.cfg @@ -22,6 +22,10 @@ vpn_network: 10.19.48.0/24 vpn_network_ipv6: 'fd9d:bc11:4020::/48' wireguard_enabled: true wireguard_port: 51820 +# If you're behind NAT or a firewall and you want to receive incoming connections long after network traffic has gone silent. +# This option will keep the "connection" open in the eyes of NAT. +# See: https://www.wireguard.com/quickstart/#nat-and-firewall-traversal-persistence +wireguard_PersistentKeepalive: 0 # Reduce the MTU of the VPN tunnel # Some cloud and internet providers use a smaller MTU (Maximum Transmission @@ -36,9 +40,9 @@ reduce_mtu: 0 # https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration strongswan_log_level: 2 -# Algo will use the following lists to block ads. You can add new block lists +# Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: -# /usr/local/sbin/adblock.sh +# /usr/local/sbin/adblock.sh # If you load very large blocklists, you may also have to modify resource limits: # /etc/systemd/system/dnsmasq.service.d/100-CustomLimitations.conf adblock_lists: diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 90da64f5..7961c6a9 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -1,3 +1,4 @@ --- +wireguard_PersistentKeepalive: 0 wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index 2aa2b3de..b601abb5 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -9,4 +9,4 @@ DNS = {{ wireguard_dns_servers }} PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} -PersistentKeepalive = 25 +{{ 'PersistentKeepalive = ' + wireguard_PersistentKeepalive|string if wireguard_PersistentKeepalive > 0 else '' }} From ec56203b8797d4af54c2ac2ed4ce5a7fa8c23415 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Mon, 25 Feb 2019 17:58:09 +0100 Subject: [PATCH 756/769] Support for custom domain names in the endpoint (#1337) --- docs/deploy-from-ansible.md | 2 +- playbooks/cloud-post.yml | 2 +- roles/local/tasks/prompts.yml | 2 +- roles/vpn/defaults/main.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/deploy-from-ansible.md b/docs/deploy-from-ansible.md index ccbb05e2..9816f0bd 100644 --- a/docs/deploy-from-ansible.md +++ b/docs/deploy-from-ansible.md @@ -248,7 +248,7 @@ You need to source the rc file prior to run Algo. Download it from the OpenStack Required variables: - server - IP or hostname to access the server via SSH -- endpoint - Public IP address of your server +- endpoint - Public IP address or domain name of your server - ssh_user diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index 283ed60a..15611539 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -1,7 +1,7 @@ --- - name: Set subjectAltName as afact set_fact: - IP_subject_alt_name: "{% if algo_provider == 'local' %}{{ IP_subject_alt_name }}{% else %}{{ cloud_instance_ip }}{% endif %}" + IP_subject_alt_name: "{{ (IP_subject_alt_name if algo_provider == 'local' else cloud_instance_ip) | lower }}" - name: Add the server to an inventory group add_host: diff --git a/roles/local/tasks/prompts.yml b/roles/local/tasks/prompts.yml index 1f5edc2e..a12b8807 100644 --- a/roles/local/tasks/prompts.yml +++ b/roles/local/tasks/prompts.yml @@ -31,7 +31,7 @@ - pause: prompt: | - Enter the public IP address of your server: (IMPORTANT! This IP is used to verify the certificate) + Enter the public IP address or domain name of your server: (IMPORTANT! This is used to verify the certificate) [{{ cloud_instance_ip }}] register: _endpoint when: endpoint is undefined diff --git a/roles/vpn/defaults/main.yml b/roles/vpn/defaults/main.yml index a865dfb4..c9b81ce0 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/vpn/defaults/main.yml @@ -35,7 +35,7 @@ algo_local_dns: false ipv6_support: false dns_encryption: true domain: false -subjectAltName_IP: "IP:{{ IP_subject_alt_name }}" +subjectAltName_IP: "{{ 'DNS:' if IP_subject_alt_name|regex_search('[a-z]') else 'IP:' }}{{ IP_subject_alt_name }}" subjectAltName_USER: "{% if '@' in item %}email:{{ item }}{% else %}DNS:{{ item }}{% endif %}" openssl_bin: openssl strongswan_enabled_plugins: From 5e5424df6922c1c7505eb08a132faedfb3fc5ff7 Mon Sep 17 00:00:00 2001 From: Demian Date: Tue, 26 Feb 2019 12:19:34 +0100 Subject: [PATCH 757/769] fix OS is undefined error (#1335) --- roles/common/tasks/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index a777eae6..fcb5af11 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -3,6 +3,8 @@ - name: Check the system raw: uname -a register: OS + tags: + - update-users - include_tasks: ubuntu.yml when: '"Ubuntu" in OS.stdout or "Linux" in OS.stdout' From b4740185e8d78f5469b3c45e91db2990c40fbe30 Mon Sep 17 00:00:00 2001 From: Tim H <6026716+tho@users.noreply.github.com> Date: Tue, 26 Feb 2019 11:40:29 -0500 Subject: [PATCH 758/769] Add catch-all VPN On Demand Rule (#739) If a user is not connected to a trusted Wi-Fi network or if the URLStringProbe fails none of the existing dictionaries match. According to the Apple Configuration Profile Reference[1] section "VPN Payload > On Demand Rules Dictionary Keys" a default behavior for unknown networks with no matching criteria should always be set as the last dictionary in the array. The current default behavior is to allow a connection to occur, but this behavior is not guaranteed. Tear down the VPN connection and do not reconnect on demand as long as the catch-all dictionary matches to guarantee the default behavior and more specifically allow users to access captive portals. [1]: https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html --- roles/vpn/templates/mobileconfig.j2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/vpn/templates/mobileconfig.j2 index b48500c2..686ed7e8 100644 --- a/roles/vpn/templates/mobileconfig.j2 +++ b/roles/vpn/templates/mobileconfig.j2 @@ -52,6 +52,10 @@ URLStringProbe http://captive.apple.com/hotspot-detect.html + + Action + Disconnect + {% else %} {% endif %} From 7e7476ec6bb02706a34e0856867e81f313d6e9f8 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Wed, 6 Mar 2019 13:04:20 +0100 Subject: [PATCH 759/769] Update cloud-pre.yml --- playbooks/cloud-pre.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index 338e70dd..6a8071b0 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -13,6 +13,7 @@ 'wireguard_enabled "{{ wireguard_enabled }}"' \ 'dns_encryption "{{ dns_encryption }}"' \ > /dev/tty + tags: debug - name: Install the requirements local_action: From 273c7665d335667097b6363fe111d6a23f176a5b Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Sun, 10 Mar 2019 18:16:34 +0100 Subject: [PATCH 760/769] Refactoring (#1334) ## Description Renames the vpn role to strongswan, and split up the variables to support 2 separate VPNs. Closes #1330 and closes #1162 Configures Ansible to use python3 on the server side. Closes #1024 Removes unneeded playbooks, reorganises a lot of variables Reorganises the `config` folder. Closes #1330

Here is how the config directory looks like now

``` configs/X.X.X.X/ |-- ipsec | |-- apple | | |-- desktop.mobileconfig | | |-- laptop.mobileconfig | | `-- phone.mobileconfig | |-- manual | | |-- cacert.pem | | |-- desktop.p12 | | |-- desktop.ssh.pem | | |-- ipsec_desktop.conf | | |-- ipsec_desktop.secrets | | |-- ipsec_laptop.conf | | |-- ipsec_laptop.secrets | | |-- ipsec_phone.conf | | |-- ipsec_phone.secrets | | |-- laptop.p12 | | |-- laptop.ssh.pem | | |-- phone.p12 | | `-- phone.ssh.pem | `-- windows | |-- desktop.ps1 | |-- laptop.ps1 | `-- phone.ps1 |-- ssh-tunnel | |-- desktop.pem | |-- desktop.pub | |-- laptop.pem | |-- laptop.pub | |-- phone.pem | |-- phone.pub | `-- ssh_config `-- wireguard |-- desktop.conf |-- desktop.png |-- laptop.conf |-- laptop.png |-- phone.conf `-- phone.png ``` ![finder](https://i.imgur.com/FtOmKO0.png)

## Motivation and Context This refactoring is focused to aim to the 1.0 release ## How Has This Been Tested? Deployed to several cloud providers with various options enabled and disabled ## Types of changes - [x] Refactoring ## Checklist: - [x] I have read the **CONTRIBUTING** document. - [x] My code follows the code style of this project. - [x] My change requires a change to the documentation. - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed. --- README.md | 8 +- config.cfg | 18 ++- input.yml | 71 +++++----- playbooks/cloud-post.yml | 2 +- playbooks/win_script_rebuild.yml | 67 --------- roles/client/tasks/main.yml | 6 +- roles/common/handlers/main.yml | 6 + roles/common/tasks/freebsd.yml | 13 +- roles/{vpn => common}/tasks/iptables.yml | 0 roles/common/tasks/ubuntu.yml | 35 ++--- roles/{vpn => common}/templates/rules.v4.j2 | 31 ++-- roles/{vpn => common}/templates/rules.v6.j2 | 24 ++-- roles/ssh_tunneling/defaults/main.yml | 2 + roles/ssh_tunneling/tasks/main.yml | 134 +++++++++++------- roles/ssh_tunneling/templates/known_hosts.j2 | 3 - roles/{vpn => strongswan}/defaults/main.yml | 32 ++--- roles/{vpn => strongswan}/handlers/main.yml | 6 - roles/{vpn => strongswan}/meta/main.yml | 0 .../tasks/client_configs.yml | 17 +-- roles/strongswan/tasks/distribute_keys.yml | 27 ++++ .../tasks/ipsec_configuration.yml | 8 +- roles/{vpn => strongswan}/tasks/main.yml | 6 - roles/{vpn => strongswan}/tasks/openssl.yml | 59 ++++---- roles/{vpn => strongswan}/tasks/ubuntu.yml | 3 - .../templates/100-CustomLimitations.conf.j2 | 0 .../templates/client_ipsec.conf.j2 | 0 .../templates/client_ipsec.secrets.j2 | 0 .../templates/client_windows.ps1.j2 | 0 .../templates/ipsec.conf.j2 | 2 +- .../templates/ipsec.secrets.j2 | 0 .../templates/mobileconfig.j2 | 0 .../templates/openssl.cnf.j2 | 0 .../templates/strongswan.conf.j2 | 0 roles/vpn/tasks/distribute_keys.yml | 27 ---- roles/wireguard/defaults/main.yml | 28 +++- roles/wireguard/tasks/keys.yml | 6 +- roles/wireguard/tasks/main.yml | 6 +- roles/wireguard/templates/client.conf.j2 | 4 +- roles/wireguard/templates/server.conf.j2 | 6 +- server.yml | 21 +-- tests/update-users.sh | 32 ++++- users.yml | 15 +- 42 files changed, 360 insertions(+), 365 deletions(-) delete mode 100644 playbooks/win_script_rebuild.yml rename roles/{vpn => common}/tasks/iptables.yml (100%) rename roles/{vpn => common}/templates/rules.v4.j2 (69%) rename roles/{vpn => common}/templates/rules.v6.j2 (77%) create mode 100644 roles/ssh_tunneling/defaults/main.yml delete mode 100644 roles/ssh_tunneling/templates/known_hosts.j2 rename roles/{vpn => strongswan}/defaults/main.yml (51%) rename roles/{vpn => strongswan}/handlers/main.yml (63%) rename roles/{vpn => strongswan}/meta/main.yml (100%) rename roles/{vpn => strongswan}/tasks/client_configs.yml (65%) create mode 100644 roles/strongswan/tasks/distribute_keys.yml rename roles/{vpn => strongswan}/tasks/ipsec_configuration.yml (87%) rename roles/{vpn => strongswan}/tasks/main.yml (84%) rename roles/{vpn => strongswan}/tasks/openssl.yml (79%) rename roles/{vpn => strongswan}/tasks/ubuntu.yml (95%) rename roles/{vpn => strongswan}/templates/100-CustomLimitations.conf.j2 (100%) rename roles/{vpn => strongswan}/templates/client_ipsec.conf.j2 (100%) rename roles/{vpn => strongswan}/templates/client_ipsec.secrets.j2 (100%) rename roles/{vpn => strongswan}/templates/client_windows.ps1.j2 (100%) rename roles/{vpn => strongswan}/templates/ipsec.conf.j2 (94%) rename roles/{vpn => strongswan}/templates/ipsec.secrets.j2 (100%) rename roles/{vpn => strongswan}/templates/mobileconfig.j2 (100%) rename roles/{vpn => strongswan}/templates/openssl.cnf.j2 (100%) rename roles/{vpn => strongswan}/templates/strongswan.conf.j2 (100%) delete mode 100644 roles/vpn/tasks/distribute_keys.yml diff --git a/README.md b/README.md index be4ce6be..2308c31c 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ Install strongSwan, then copy the included ipsec_user.conf, ipsec_user.secrets, #### Ubuntu Server 18.04 example 1. `sudo apt-get install strongswan libstrongswan-standard-plugins`: install strongSwan -2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//pki/certs/.crt` -3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//pki/private/.key` -4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//pki/cacert.pem` +2. `/etc/ipsec.d/certs`: copy `.crt` from `algo-master/configs//ipsec/manual/.crt` +3. `/etc/ipsec.d/private`: copy `.key` from `algo-master/configs//ipsec/manual/.key` +4. `/etc/ipsec.d/cacerts`: copy `cacert.pem` from `algo-master/configs//ipsec/manual/cacert.pem` 5. `/etc/ipsec.secrets`: add your `user.key` to the list, e.g. ` : ECDSA .key` 6. `/etc/ipsec.conf`: add the connection from `ipsec_user.conf` and ensure `leftcert` matches the `.crt` filename 7. `sudo ipsec restart`: pick up config changes @@ -160,7 +160,7 @@ If you turned on the optional SSH tunneling role, then local user accounts will Use the example command below to start an SSH tunnel by replacing `user` and `ip` with your own. Once the tunnel is setup, you can configure a browser or other application to use 127.0.0.1:1080 as a SOCKS proxy to route traffic through the Algo server. - `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs/ip_user.ssh.pem` + `ssh -D 127.0.0.1:1080 -f -q -C -N user@ip -i configs//ssh-tunnel/.pem` ## SSH into Algo Server diff --git a/config.cfg b/config.cfg index 15aa586e..bfcaac68 100644 --- a/config.cfg +++ b/config.cfg @@ -18,8 +18,14 @@ keys_clean_all: False # Clean up cloud python environments clean_environment: false -vpn_network: 10.19.48.0/24 -vpn_network_ipv6: 'fd9d:bc11:4020::/48' +# Deploy StrongSwan to enable IPsec support +ipsec_enabled: true + +# StrongSwan log level +# https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration +strongswan_log_level: 2 + +# Deploy WireGuard wireguard_enabled: true wireguard_port: 51820 # If you're behind NAT or a firewall and you want to receive incoming connections long after network traffic has gone silent. @@ -36,10 +42,6 @@ wireguard_PersistentKeepalive: 0 # See: https://github.com/trailofbits/algo/blob/master/docs/troubleshooting.md#various-websites-appear-to-be-offline-through-the-vpn reduce_mtu: 0 -# StrongSwan log level -# https://wiki.strongswan.org/projects/strongswan/wiki/LoggerConfiguration -strongswan_log_level: 2 - # Algo will use the following lists to block ads. You can add new block lists # after deployment by modifying the line starting "BLOCKLIST_URLS=" at: # /usr/local/sbin/adblock.sh @@ -90,10 +92,6 @@ unattended_reboot: enabled: false time: 06:00 -pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" -VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" -CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" - # Block traffic between connected clients BetweenClients_DROP: true diff --git a/input.yml b/input.yml index 34ae4f66..5cc60170 100644 --- a/input.yml +++ b/input.yml @@ -48,30 +48,45 @@ when: - server_name is undefined - algo_provider != "local" + - block: + - pause: + prompt: | + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? + [y/N] + register: _ondemand_cellular + when: ondemand_cellular is undefined - - pause: - prompt: | - Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to cellular networks? - [y/N] - register: _ondemand_cellular - when: ondemand_cellular is undefined + - pause: + prompt: | + Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? + [y/N] + register: _ondemand_wifi + when: ondemand_wifi is undefined - - pause: - prompt: | - Do you want macOS/iOS IPsec clients to enable "Connect On Demand" when connected to Wi-Fi? - [y/N] - register: _ondemand_wifi - when: ondemand_wifi is undefined + - pause: + prompt: | + List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" + (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) + register: _ondemand_wifi_exclude + when: + - ondemand_wifi_exclude is undefined + - (ondemand_wifi|default(false)|bool) or + (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) - - pause: - prompt: | - List the names of any trusted Wi-Fi networks where macOS/iOS IPsec clients should not use "Connect On Demand" - (e.g., your home network. Comma-separated value, e.g., HomeNet,OfficeWifi,AlgoWiFi) - register: _ondemand_wifi_exclude - when: - - ondemand_wifi_exclude is undefined - - (ondemand_wifi|default(false)|bool) or - (booleans_map[_ondemand_wifi.user_input|default(omit)]|default(false)) + - pause: + prompt: | + Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) + [y/N] + register: _windows + when: windows is undefined + + - pause: + prompt: | + Do you want to retain the CA key? (required to add users in the future, but less secure) + [y/N] + register: _store_cakey + when: store_cakey is undefined + when: ipsec_enabled - pause: prompt: | @@ -87,20 +102,6 @@ register: _ssh_tunneling when: ssh_tunneling is undefined - - pause: - prompt: | - Do you want the VPN to support Windows 10 or Linux Desktop clients? (enables compatible ciphers and key exchange, less secure) - [y/N] - register: _windows - when: windows is undefined - - - pause: - prompt: | - Do you want to retain the CA key? (required to add users in the future, but less secure) - [y/N] - register: _store_cakey - when: store_cakey is undefined - - name: Set facts based on the input set_fact: algo_server_name: >- diff --git a/playbooks/cloud-post.yml b/playbooks/cloud-post.yml index 15611539..0ada114d 100644 --- a/playbooks/cloud-post.yml +++ b/playbooks/cloud-post.yml @@ -9,7 +9,7 @@ groups: vpn-host ansible_connection: "{% if cloud_instance_ip == 'localhost' %}local{% else %}ssh{% endif %}" ansible_ssh_user: "{{ ansible_ssh_user }}" - ansible_python_interpreter: "/usr/bin/python2.7" + ansible_python_interpreter: "/usr/bin/python3" algo_provider: "{{ algo_provider }}" algo_server_name: "{{ algo_server_name }}" algo_ondemand_cellular: "{{ algo_ondemand_cellular }}" diff --git a/playbooks/win_script_rebuild.yml b/playbooks/win_script_rebuild.yml deleted file mode 100644 index 12bdfe91..00000000 --- a/playbooks/win_script_rebuild.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- - -# This playbook is designed to help when modifying the Windows script template -# in roles/vpn/templates/client_windows.ps1.j2 -# It rebuilds the client_USER.ps1 scripts for each user defined in config.cfg, -# without redeploying users or opening an SSH connection to the Algo server at -# all. -# -# This playbook is _not_ part of a normal Algo deployment. -# It is only intended to speed up development of the client_USER.ps1 Windows -# Algo install scripts. -# -# REQUIREMENTS -# - Algo must have been deployed once -# - Windows users must have been enabled at deployment time -# - All users defined in config.cfg must not have changed -# - Only one Algo deployment exists in the configs/ directory -# - There must be exactly one subfolder in the configs/ directory: -# the folder named after the IP of the algo server - -- hosts: localhost - gather_facts: False - tags: always - vars_files: - - ../config.cfg - - tasks: - - - name: Get config subdir - shell: find ../configs/* -maxdepth 0 -type d | sed 's/.*\///' - register: config_subdir_result - - fail: - msg: - - "Found wrong number of config subdirs... stdout:" - - "{{ config_subdir_result.split('\n') }}" - when: config_subdir_result.stdout.split('\n') | length != 1 - - set_fact: - IP_subject_alt_name: "{{ config_subdir_result.stdout }}" - - debug: - var: IP_subject_alt_name - - - name: Register p12 PayloadContent - shell: cat private/{{ item }}.p12 | base64 - register: PayloadContent - args: - chdir: "../configs/{{ IP_subject_alt_name }}/pki/" - with_items: "{{ users }}" - - - name: Set facts for mobileconfigs - set_fact: - proxy_enabled: false - PayloadContentCA: "{{ lookup('file' , '../configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" - - - name: Build the windows client powershell script - template: - src: ../roles/vpn/templates/client_windows.ps1.j2 - dest: ../configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 - mode: 0600 - with_together: - - "{{ users }}" - - "{{ PayloadContent.results }}" - - - name: List windows client powershell scripts - debug: - msg: "configs/{{ IP_subject_alt_name }}/windows_{{ item }}.ps1" - with_items: - - "{{ users }}" diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 60fafed2..e3b1634d 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -50,11 +50,11 @@ src: "{{ item.src }}" dest: "{{ item.dest }}" with_items: - - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ vpn_user }}.crt" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/certs/{{ vpn_user }}.crt" dest: "{{ configs_prefix }}/ipsec.d/certs/{{ vpn_user }}.crt" - - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/cacert.pem" dest: "{{ configs_prefix }}/ipsec.d/cacerts/{{ IP_subject_alt_name }}.pem" - - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ vpn_user }}.key" + - src: "configs/{{ IP_subject_alt_name }}/ipsec/.pki/private/{{ vpn_user }}.key" dest: "{{ configs_prefix }}/ipsec.d/private/{{ vpn_user }}.key" notify: - restart strongswan diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml index 1415245e..06f5080d 100644 --- a/roles/common/handlers/main.yml +++ b/roles/common/handlers/main.yml @@ -19,3 +19,9 @@ ifconfig lo100 create && ifconfig lo100 inet {{ local_service_ip }} netmask 255.255.255.255 && ifconfig lo100 inet6 FCAA::1/64; echo $? + +- name: save iptables + shell: service netfilter-persistent save + +- name: restart iptables + service: name=netfilter-persistent state=restarted diff --git a/roles/common/tasks/freebsd.yml b/roles/common/tasks/freebsd.yml index 70ebe8fa..81362632 100644 --- a/roles/common/tasks/freebsd.yml +++ b/roles/common/tasks/freebsd.yml @@ -1,4 +1,16 @@ --- +- name: FreeBSD | Install prerequisites + package: + name: + - python3 + - sudo + vars: + ansible_python_interpreter: /usr/local/bin/python2.7 + +- name: Set python3 as the interpreter to use + set_fact: + ansible_python_interpreter: /usr/local/bin/python3 + - name: Gather facts setup: @@ -15,7 +27,6 @@ strongswan_additional_plugins: - kernel-pfroute - kernel-pfkey - ansible_python_interpreter: /usr/local/bin/python2.7 tools: - git - subversion diff --git a/roles/vpn/tasks/iptables.yml b/roles/common/tasks/iptables.yml similarity index 100% rename from roles/vpn/tasks/iptables.yml rename to roles/common/tasks/iptables.yml diff --git a/roles/common/tasks/ubuntu.yml b/roles/common/tasks/ubuntu.yml index 37d469e8..a37a8c00 100644 --- a/roles/common/tasks/ubuntu.yml +++ b/roles/common/tasks/ubuntu.yml @@ -1,24 +1,4 @@ --- -- block: - - name: Ubuntu | Install prerequisites - apt: - name: "{{ item }}" - update_cache: true - with_items: - - python2.7 - - sudo - - - name: Ubuntu | Configure defaults - alternatives: - name: python - link: /usr/bin/python - path: /usr/bin/python2.7 - priority: 1 - tags: - - update-alternatives - vars: - ansible_python_interpreter: /usr/bin/python3 - - name: Gather facts setup: @@ -115,15 +95,20 @@ value: 1 - name: Install tools - package: name="{{ item }}" state=present + apt: + name: "{{ item }}" + state: present + update_cache: true with_items: - "{{ tools|default([]) }}" - name: Install headers apt: - name: "{{ item }}" + name: + - linux-headers-generic + - "linux-headers-{{ ansible_kernel }}" state: present when: install_headers - with_items: - - linux-headers-generic - - "linux-headers-{{ ansible_kernel }}" + +- include_tasks: iptables.yml + tags: iptables diff --git a/roles/vpn/templates/rules.v4.j2 b/roles/common/templates/rules.v4.j2 similarity index 69% rename from roles/vpn/templates/rules.v4.j2 rename to roles/common/templates/rules.v4.j2 index 1b487e6d..d71f51fb 100644 --- a/roles/vpn/templates/rules.v4.j2 +++ b/roles/common/templates/rules.v4.j2 @@ -1,3 +1,6 @@ +{% set subnets = ([strongswan_network] if ipsec_enabled else []) + ([wireguard_network_ipv4] if wireguard_enabled else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled else []) + ([wireguard_port] if wireguard_enabled else []) %} + #### The mangle table # This table allows us to modify packet headers # Packets enter this table first @@ -10,8 +13,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu|int > 0 %} --A FORWARD -s {{ vpn_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} +{% if reduce_mtu|int > 0 and ipsec_enabled %} +-A FORWARD -s {{ strongswan_network }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1360 - reduce_mtu|int }} {% endif %} COMMIT @@ -27,7 +30,7 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE COMMIT @@ -54,12 +57,15 @@ COMMIT -A INPUT -p ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmp --icmp-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Accept IPSEC/WireGuard traffic to ports {{ subnets|join(',') }} +-A INPUT -p udp -m multiport --dports {{ ports|join(',') }} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT -# Allow any traffic from the VPN + +{% if ipsec_enabled %} +# Allow any traffic from the IPsec VPN -A INPUT -p ipencap -m policy --dir in --pol ipsec --proto esp -j ACCEPT +{% endif %} # TODO: # The IP of the resolver should be bound to a DUMMY interface. @@ -70,10 +76,7 @@ COMMIT -A INPUT -d {{ local_service_ip }} -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP %} -{% set BetweenClientsPolicy = "DROP" %} -{% endif %} --A FORWARD -s {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -d {{ vpn_network }}{% if wireguard_enabled %},{{ wireguard_vpn_network }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} +-A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }} # Forward any packet that's part of an established connection -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT @@ -83,12 +86,14 @@ COMMIT -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP +{% if ipsec_enabled %} # Forward any IPSEC traffic from the VPN network --A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network }} -m policy --pol ipsec --dir in -j ACCEPT +-A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network }} -m policy --pol ipsec --dir in -j ACCEPT +{% endif %} -# Forward any traffic from the WireGuard VPN network {% if wireguard_enabled %} --A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network }} -m policy --pol none --dir in -j ACCEPT +# Forward any traffic from the WireGuard VPN network +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv4 }} -m policy --pol none --dir in -j ACCEPT {% endif %} COMMIT diff --git a/roles/vpn/templates/rules.v6.j2 b/roles/common/templates/rules.v6.j2 similarity index 77% rename from roles/vpn/templates/rules.v6.j2 rename to roles/common/templates/rules.v6.j2 index 6095e211..12bed2b4 100644 --- a/roles/vpn/templates/rules.v6.j2 +++ b/roles/common/templates/rules.v6.j2 @@ -1,3 +1,6 @@ +{% set subnets = ([strongswan_network_ipv6] if ipsec_enabled else []) + ([wireguard_network_ipv6] if wireguard_enabled else []) %} +{% set ports = (['500', '4500'] if ipsec_enabled else []) + ([wireguard_port] if wireguard_enabled else []) %} + #### The mangle table # This table allows us to modify packet headers # Packets enter this table first @@ -10,8 +13,8 @@ :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -{% if reduce_mtu|int > 0 %} --A FORWARD -s {{ vpn_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} +{% if reduce_mtu|int > 0 and ipsec_enabled %} +-A FORWARD -s {{ strongswan_network_ipv6 }} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss {{ 1340 - reduce_mtu|int }} {% endif %} COMMIT @@ -26,7 +29,7 @@ COMMIT :POSTROUTING ACCEPT [0:0] # Allow traffic from the VPN network to the outside world, and replies --A POSTROUTING -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -m policy --pol none --dir out -j MASQUERADE +-A POSTROUTING -s {{ subnets|join(',') }} -m policy --pol none --dir out -j MASQUERADE COMMIT @@ -60,8 +63,8 @@ COMMIT -A INPUT -m ah -j ACCEPT # rate limit ICMP traffic per source -A INPUT -p icmpv6 --icmpv6-type echo-request -m hashlimit --hashlimit-upto 5/s --hashlimit-mode srcip --hashlimit-srcmask 32 --hashlimit-name icmp-echo-drop -j ACCEPT -# Accept IPSEC traffic to ports 500 (IPSEC) and 4500 (MOBIKE aka IKE + NAT traversal) --A INPUT -p udp -m multiport --dports 500,4500{% if wireguard_enabled %},{{ wireguard_port}}{% endif %} -j ACCEPT +# Accept IPSEC/WireGuard traffic to ports {{ subnets|join(',') }} +-A INPUT -p udp -m multiport --dports {{ ports|join(',') }} -j ACCEPT # Allow new traffic to port 22 (SSH) -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT @@ -83,19 +86,18 @@ COMMIT -A INPUT -d fcaa::1 -p udp --dport 53 -j ACCEPT # Drop traffic between VPN clients -{% if BetweenClients_DROP %} -{% set BetweenClientsPolicy = "DROP" %} -{% endif %} --A FORWARD -s {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -d {{ vpn_network_ipv6 }}{% if wireguard_enabled %},{{ wireguard_vpn_network_ipv6 }}{% endif %} -j {{ BetweenClientsPolicy | default("ACCEPT") }} +-A FORWARD -s {{ subnets|join(',') }} -d {{ subnets|join(',') }} -j {{ "DROP" if BetweenClients_DROP else "ACCEPT" }} -A FORWARD -j ICMPV6-CHECK -A FORWARD -p tcp --dport 445 -j DROP -A FORWARD -p udp -m multiport --ports 137,138 -j DROP -A FORWARD -p tcp -m multiport --ports 137,139 -j DROP -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT --A FORWARD -m conntrack --ctstate NEW -s {{ vpn_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +{% if ipsec_enabled %} +-A FORWARD -m conntrack --ctstate NEW -s {{ strongswan_network_ipv6 }} -m policy --pol ipsec --dir in -j ACCEPT +{% endif %} {% if wireguard_enabled %} --A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_vpn_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT +-A FORWARD -m conntrack --ctstate NEW -s {{ wireguard_network_ipv6 }} -m policy --pol none --dir in -j ACCEPT {% endif %} # Use the ICMPV6-CHECK chain, described above diff --git a/roles/ssh_tunneling/defaults/main.yml b/roles/ssh_tunneling/defaults/main.yml new file mode 100644 index 00000000..3ed9b592 --- /dev/null +++ b/roles/ssh_tunneling/defaults/main.yml @@ -0,0 +1,2 @@ +--- +ssh_tunnels_config_path: "configs/{{ IP_subject_alt_name }}/ssh-tunnel/" diff --git a/roles/ssh_tunneling/tasks/main.yml b/roles/ssh_tunneling/tasks/main.yml index 259464b4..c52840f6 100644 --- a/roles/ssh_tunneling/tasks/main.yml +++ b/roles/ssh_tunneling/tasks/main.yml @@ -25,65 +25,93 @@ owner: root group: "{{ root_group|default('root') }}" - - name: Ensure that the SSH users exist - user: - name: "{{ item }}" - groups: algo - home: '/var/jail/{{ item }}' - createhome: yes - generate_ssh_key: false - shell: /bin/false - state: present - append: yes - with_items: "{{ users }}" - tags: update-users + - block: + - name: Ensure that the SSH users exist + user: + name: "{{ item }}" + groups: algo + home: '/var/jail/{{ item }}' + createhome: yes + generate_ssh_key: false + shell: /bin/false + state: present + append: yes + with_items: "{{ users }}" - - name: The authorized keys file created - authorized_key: - user: "{{ item }}" - key: "{{ lookup('file', 'configs/' + IP_subject_alt_name + '/pki/public/' + item + '.pub') }}" - state: present - manage_dir: true - exclusive: true - with_items: "{{ users }}" - tags: update-users + - block: + - name: Clean up the ssh-tunnel directory + file: + dest: "{{ ssh_tunnels_config_path }}" + state: absent + when: keys_clean_all|bool == True - - name: Generate SSH fingerprints - shell: ssh-keyscan {{ IP_subject_alt_name }} 2>/dev/null - register: ssh_fingerprints + - name: Ensure the config directories exist + file: + dest: "{{ ssh_tunnels_config_path }}" + state: directory + recurse: yes + mode: '0700' - - name: Fetch the known_hosts file - local_action: - module: template - src: known_hosts.j2 - dest: configs/{{ IP_subject_alt_name }}/known_hosts - become: no + - name: Check if the private keys exist + stat: + path: "{{ ssh_tunnels_config_path }}/{{ item }}.pem" + register: privatekey + with_items: "{{ users }}" - - name: Build the client ssh config - local_action: - module: template - src: ssh_config.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item }}.ssh_config - mode: 0600 - become: false - tags: update-users - with_items: "{{ users }}" + - name: Build ssh private keys + openssl_privatekey: + path: "{{ ssh_tunnels_config_path }}/{{ item.item }}.pem" + passphrase: "{{ p12_export_password }}" + cipher: aes256 + force: false + no_log: true + when: not item.stat.exists + with_items: "{{ privatekey.results }}" + register: openssl_privatekey - - name: Get active users - getent: - database: group - key: algo - split: ':' - tags: update-users + - name: Build ssh public keys + openssl_publickey: + path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pub" + privatekey_path: "{{ ssh_tunnels_config_path }}/{{ item.item.item }}.pem" + privatekey_passphrase: "{{ p12_export_password }}" + format: OpenSSH + force: true + no_log: true + when: item.changed + with_items: "{{ openssl_privatekey.results }}" - - name: Delete non-existing users - user: - name: "{{ item }}" - state: absent - remove: yes - force: yes - when: item not in users - with_items: "{{ getent_group['algo'][2].split(',') }}" + - name: Build the client ssh config + template: + src: ssh_config.j2 + dest: "{{ ssh_tunnels_config_path }}/{{ item }}.ssh_config" + mode: 0700 + with_items: "{{ users }}" + delegate_to: localhost + become: false + + - name: The authorized keys file created + authorized_key: + user: "{{ item }}" + key: "{{ lookup('file', ssh_tunnels_config_path + '/' + item + '.pub') }}" + state: present + manage_dir: true + exclusive: true + with_items: "{{ users }}" + + - name: Get active users + getent: + database: group + key: algo + split: ':' + + - name: Delete non-existing users + user: + name: "{{ item }}" + state: absent + remove: yes + force: yes + when: item not in users + with_items: "{{ getent_group['algo'][2].split(',') }}" tags: update-users rescue: - debug: var=fail_hint diff --git a/roles/ssh_tunneling/templates/known_hosts.j2 b/roles/ssh_tunneling/templates/known_hosts.j2 deleted file mode 100644 index 98d33c4d..00000000 --- a/roles/ssh_tunneling/templates/known_hosts.j2 +++ /dev/null @@ -1,3 +0,0 @@ -{% for item in ssh_fingerprints.stdout_lines %} -{{ item }} -{% endfor %} diff --git a/roles/vpn/defaults/main.yml b/roles/strongswan/defaults/main.yml similarity index 51% rename from roles/vpn/defaults/main.yml rename to roles/strongswan/defaults/main.yml index c9b81ce0..b8969332 100644 --- a/roles/vpn/defaults/main.yml +++ b/roles/strongswan/defaults/main.yml @@ -1,31 +1,11 @@ --- +ipsec_config_path: "configs/{{ IP_subject_alt_name }}/ipsec/" +ipsec_pki_path: "{{ ipsec_config_path }}/.pki/" +strongswan_network: 10.19.48.0/24 +strongswan_network_ipv6: 'fd9d:bc11:4020::/48' strongswan_shell: /usr/sbin/nologin strongswan_home: /var/lib/strongswan BetweenClients_DROP: true -wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" -wireguard_interface: wg0 -wireguard_network_ipv4: - subnet: 10.19.49.0 - prefix: 24 - gateway: 10.19.49.1 - clients_range: 10.19.49 - clients_start: 2 -wireguard_network_ipv6: - subnet: 'fd9d:bc11:4021::' - prefix: 48 - gateway: 'fd9d:bc11:4021::1' - clients_range: 'fd9d:bc11:4021::' - clients_start: 2 -wireguard_vpn_network: "{{ wireguard_network_ipv4['subnet'] }}/{{ wireguard_network_ipv4['prefix'] }}" -wireguard_vpn_network_ipv6: "{{ wireguard_network_ipv6['subnet'] }}/{{ wireguard_network_ipv6['prefix'] }}" -keys_clean_all: false -wireguard_dns_servers: >- - {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} - {{ local_service_ip }} - {% else %} - {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} - {% endif %} - algo_ondemand_cellular: false algo_ondemand_wifi: false algo_ondemand_wifi_exclude: '_null' @@ -65,3 +45,7 @@ ciphers: compat: ike: aes256gcm16-prfsha512-ecp384,aes256-sha2_512-prfsha512-ecp384,aes256-sha2_384-prfsha384-ecp384! esp: aes256gcm16-ecp384,aes256-sha2_512-prfsha512-ecp384! + +pkcs12_PayloadCertificateUUID: "{{ 900000 | random | to_uuid | upper }}" +VPN_PayloadIdentifier: "{{ 800000 | random | to_uuid | upper }}" +CA_PayloadIdentifier: "{{ 700000 | random | to_uuid | upper }}" diff --git a/roles/vpn/handlers/main.yml b/roles/strongswan/handlers/main.yml similarity index 63% rename from roles/vpn/handlers/main.yml rename to roles/strongswan/handlers/main.yml index 8ce3163a..6e7968f4 100644 --- a/roles/vpn/handlers/main.yml +++ b/roles/strongswan/handlers/main.yml @@ -7,11 +7,5 @@ - name: restart apparmor service: name=apparmor state=restarted -- name: save iptables - shell: service netfilter-persistent save - -- name: restart iptables - service: name=netfilter-persistent state=restarted - - name: rereadcrls shell: ipsec rereadcrls; ipsec purgecrls diff --git a/roles/vpn/meta/main.yml b/roles/strongswan/meta/main.yml similarity index 100% rename from roles/vpn/meta/main.yml rename to roles/strongswan/meta/main.yml diff --git a/roles/vpn/tasks/client_configs.yml b/roles/strongswan/tasks/client_configs.yml similarity index 65% rename from roles/vpn/tasks/client_configs.yml rename to roles/strongswan/tasks/client_configs.yml index 827bef76..de4ff0f8 100644 --- a/roles/vpn/tasks/client_configs.yml +++ b/roles/strongswan/tasks/client_configs.yml @@ -1,20 +1,19 @@ --- - - name: Register p12 PayloadContent shell: cat private/{{ item }}.p12 | base64 register: PayloadContent args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" with_items: "{{ users }}" - name: Set facts for mobileconfigs set_fact: - PayloadContentCA: "{{ lookup('file' , 'configs/{{ IP_subject_alt_name }}/pki/cacert.pem')|b64encode }}" + PayloadContentCA: "{{ lookup('file' , '{{ ipsec_pki_path }}/cacert.pem')|b64encode }}" - name: Build the mobileconfigs template: src: mobileconfig.j2 - dest: configs/{{ IP_subject_alt_name }}/{{ item.0 }}.mobileconfig + dest: "{{ ipsec_config_path }}/apple/{{ item.0 }}.mobileconfig" mode: 0600 with_together: - "{{ users }}" @@ -24,7 +23,7 @@ - name: Build the client ipsec config file template: src: client_ipsec.conf.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.conf + dest: "{{ ipsec_config_path }}/manual/{{ item }}.conf" mode: 0600 with_items: - "{{ users }}" @@ -32,7 +31,7 @@ - name: Build the client ipsec secret file template: src: client_ipsec.secrets.j2 - dest: configs/{{ IP_subject_alt_name }}/ipsec_{{ item }}.secrets + dest: "{{ ipsec_config_path }}/manual/{{ item }}.secrets" mode: 0600 with_items: - "{{ users }}" @@ -40,7 +39,7 @@ - name: Build the windows client powershell script template: src: client_windows.ps1.j2 - dest: configs/{{ IP_subject_alt_name }}/windows_{{ item.0 }}.ps1 + dest: "{{ ipsec_config_path }}/windows/{{ item.0 }}.ps1" mode: 0600 when: algo_windows with_together: @@ -49,8 +48,6 @@ - name: Restrict permissions for the local private directories file: - path: "{{ item }}" + path: "{{ ipsec_config_path }}" state: directory mode: 0700 - with_items: - - configs/{{ IP_subject_alt_name }} diff --git a/roles/strongswan/tasks/distribute_keys.yml b/roles/strongswan/tasks/distribute_keys.yml new file mode 100644 index 00000000..02df30bf --- /dev/null +++ b/roles/strongswan/tasks/distribute_keys.yml @@ -0,0 +1,27 @@ +--- + +- name: Copy the keys to the strongswan directory + copy: + src: "{{ ipsec_pki_path }}/{{ item.src }}" + dest: "{{ config_prefix|default('/') }}etc/ipsec.d/{{ item.dest }}" + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + with_items: + - src: "cacert.pem" + dest: "cacerts/ca.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "certs/{{ IP_subject_alt_name }}.crt" + dest: "certs/{{ IP_subject_alt_name }}.crt" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + - src: "private/{{ IP_subject_alt_name }}.key" + dest: "private/{{ IP_subject_alt_name }}.key" + owner: strongswan + group: "{{ root_group|default('root') }}" + mode: "0600" + notify: + - restart strongswan diff --git a/roles/vpn/tasks/ipsec_configuration.yml b/roles/strongswan/tasks/ipsec_configuration.yml similarity index 87% rename from roles/vpn/tasks/ipsec_configuration.yml rename to roles/strongswan/tasks/ipsec_configuration.yml index cc7c21ec..ce5b3e5b 100644 --- a/roles/vpn/tasks/ipsec_configuration.yml +++ b/roles/strongswan/tasks/ipsec_configuration.yml @@ -3,23 +3,23 @@ - name: Setup the config files from our templates template: src: "{{ item.src }}" - dest: "{{ item.dest }}" + dest: "{{ config_prefix|default('/') }}etc/{{ item.dest }}" owner: "{{ item.owner }}" group: "{{ item.group }}" mode: "{{ item.mode }}" with_items: - src: strongswan.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/strongswan.conf" + dest: "strongswan.conf" owner: root group: "{{ root_group|default('root') }}" mode: "0644" - src: ipsec.conf.j2 - dest: "{{ config_prefix|default('/') }}etc/ipsec.conf" + dest: "ipsec.conf" owner: root group: "{{ root_group|default('root') }}" mode: "0644" - src: ipsec.secrets.j2 - dest: "{{ config_prefix|default('/') }}etc/ipsec.secrets" + dest: "ipsec.secrets" owner: strongswan group: "{{ root_group|default('root') }}" mode: "0600" diff --git a/roles/vpn/tasks/main.yml b/roles/strongswan/tasks/main.yml similarity index 84% rename from roles/vpn/tasks/main.yml rename to roles/strongswan/tasks/main.yml index bfe929ca..6b9699e9 100644 --- a/roles/vpn/tasks/main.yml +++ b/roles/strongswan/tasks/main.yml @@ -1,11 +1,5 @@ --- - block: - - name: Include WireGuard role - include_role: - name: wireguard - tags: wireguard - when: wireguard_enabled and ansible_distribution == 'Ubuntu' - - include_tasks: ubuntu.yml when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu' diff --git a/roles/vpn/tasks/openssl.yml b/roles/strongswan/tasks/openssl.yml similarity index 79% rename from roles/vpn/tasks/openssl.yml rename to roles/strongswan/tasks/openssl.yml index 3a286be7..694bb83c 100644 --- a/roles/vpn/tasks/openssl.yml +++ b/roles/strongswan/tasks/openssl.yml @@ -7,13 +7,13 @@ - name: Ensure the pki directory does not exist file: - dest: configs/{{ IP_subject_alt_name }}/pki + dest: "{{ ipsec_pki_path }}" state: absent when: keys_clean_all|bool == True - name: Ensure the pki directories exist file: - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + dest: "{{ ipsec_pki_path }}/{{ item }}" state: directory recurse: yes mode: '0700' @@ -26,9 +26,20 @@ - public - reqs + - name: Ensure the config directories exist + file: + dest: "{{ ipsec_config_path }}/{{ item }}" + state: directory + recurse: yes + mode: '0700' + with_items: + - apple + - windows + - manual + - name: Ensure the files exist file: - dest: "configs/{{ IP_subject_alt_name }}/pki/{{ item }}" + dest: "{{ ipsec_pki_path }}/{{ item }}" state: touch with_items: - ".rnd" @@ -40,7 +51,7 @@ - name: Generate the openssl server configs template: src: openssl.cnf.j2 - dest: "configs/{{ IP_subject_alt_name }}/pki/openssl.cnf" + dest: "{{ ipsec_pki_path }}/openssl.cnf" - name: Build the CA pair shell: > @@ -55,20 +66,19 @@ -passout pass:"{{ CA_password }}" && touch {{ IP_subject_alt_name }}_ca_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: "{{ IP_subject_alt_name }}_ca_generated" executable: bash - name: Copy the CA certificate copy: - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "configs/{{ IP_subject_alt_name }}/cacert.pem" - mode: 0600 + src: "{{ ipsec_pki_path }}/cacert.pem" + dest: "{{ ipsec_config_path }}/manual/cacert.pem" - name: Generate the serial number shell: echo 01 > serial && touch serial_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: serial_generated - name: Build the server pair @@ -90,7 +100,7 @@ -subj "/CN={{ IP_subject_alt_name }}" && touch certs/{{ IP_subject_alt_name }}_crt_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: certs/{{ IP_subject_alt_name }}_crt_generated executable: bash @@ -113,23 +123,15 @@ -subj "/CN={{ item }}" && touch certs/{{ item }}_crt_generated args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" creates: certs/{{ item }}_crt_generated executable: bash with_items: "{{ users }}" - - name: Create links for the private keys - file: - src: "pki/private/{{ item }}.key" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.ssh.pem" - state: link - force: true - with_items: "{{ users }}" - - name: Build openssh public keys openssl_publickey: - path: "configs/{{ IP_subject_alt_name }}/pki/public/{{ item }}.pub" - privatekey_path: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.key" + path: "{{ ipsec_pki_path }}/public/{{ item }}.pub" + privatekey_path: "{{ ipsec_pki_path }}/private/{{ item }}.key" format: OpenSSH with_items: "{{ users }}" @@ -144,16 +146,15 @@ -out private/{{ item }}.p12 -passout pass:"{{ p12_export_password }}" args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path }}" executable: bash with_items: "{{ users }}" register: p12 - name: Copy the p12 certificates copy: - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ item }}.p12" - dest: "configs/{{ IP_subject_alt_name }}/{{ item }}.p12" - mode: 0600 + src: "{{ ipsec_pki_path }}/private/{{ item }}.p12" + dest: "{{ ipsec_config_path }}/manual/{{ item }}.p12" with_items: - "{{ users }}" @@ -164,7 +165,7 @@ awk '{print $5}' | sed 's/\/CN=//g' args: - chdir: "configs/{{ IP_subject_alt_name }}/pki/" + chdir: "{{ ipsec_pki_path}}" register: valid_certs - name: Revoke non-existing users @@ -176,7 +177,7 @@ -out crl/{{ item }}.crt register: gencrl args: - chdir: configs/{{ IP_subject_alt_name }}/pki/ + chdir: "{{ ipsec_pki_path }}" creates: crl/{{ item }}.crt executable: bash when: item not in users @@ -192,7 +193,7 @@ - gencrl is defined - gencrl.changed args: - chdir: configs/{{ IP_subject_alt_name }}/pki/ + chdir: "{{ ipsec_pki_path }}" executable: bash delegate_to: localhost become: no @@ -201,7 +202,7 @@ - name: Copy the CRL to the vpn server copy: - src: configs/{{ IP_subject_alt_name }}/pki/crl/algo.root.pem + src: "{{ ipsec_pki_path }}/crl/algo.root.pem" dest: "{{ config_prefix|default('/') }}etc/ipsec.d/crls/algo.root.pem" when: - gencrl is defined diff --git a/roles/vpn/tasks/ubuntu.yml b/roles/strongswan/tasks/ubuntu.yml similarity index 95% rename from roles/vpn/tasks/ubuntu.yml rename to roles/strongswan/tasks/ubuntu.yml index 6f585441..41b58352 100644 --- a/roles/vpn/tasks/ubuntu.yml +++ b/roles/strongswan/tasks/ubuntu.yml @@ -43,6 +43,3 @@ notify: - daemon-reload - restart strongswan - -- include_tasks: iptables.yml - tags: iptables diff --git a/roles/vpn/templates/100-CustomLimitations.conf.j2 b/roles/strongswan/templates/100-CustomLimitations.conf.j2 similarity index 100% rename from roles/vpn/templates/100-CustomLimitations.conf.j2 rename to roles/strongswan/templates/100-CustomLimitations.conf.j2 diff --git a/roles/vpn/templates/client_ipsec.conf.j2 b/roles/strongswan/templates/client_ipsec.conf.j2 similarity index 100% rename from roles/vpn/templates/client_ipsec.conf.j2 rename to roles/strongswan/templates/client_ipsec.conf.j2 diff --git a/roles/vpn/templates/client_ipsec.secrets.j2 b/roles/strongswan/templates/client_ipsec.secrets.j2 similarity index 100% rename from roles/vpn/templates/client_ipsec.secrets.j2 rename to roles/strongswan/templates/client_ipsec.secrets.j2 diff --git a/roles/vpn/templates/client_windows.ps1.j2 b/roles/strongswan/templates/client_windows.ps1.j2 similarity index 100% rename from roles/vpn/templates/client_windows.ps1.j2 rename to roles/strongswan/templates/client_windows.ps1.j2 diff --git a/roles/vpn/templates/ipsec.conf.j2 b/roles/strongswan/templates/ipsec.conf.j2 similarity index 94% rename from roles/vpn/templates/ipsec.conf.j2 rename to roles/strongswan/templates/ipsec.conf.j2 index 086e18af..68fa3464 100644 --- a/roles/vpn/templates/ipsec.conf.j2 +++ b/roles/strongswan/templates/ipsec.conf.j2 @@ -27,7 +27,7 @@ conn %default right=%any rightauth=pubkey - rightsourceip={{ vpn_network }},{{ vpn_network_ipv6 }} + rightsourceip={{ strongswan_network }},{{ strongswan_network_ipv6 }} {% if algo_local_dns or dns_encryption %} rightdns={{ local_service_ip }} {% else %} diff --git a/roles/vpn/templates/ipsec.secrets.j2 b/roles/strongswan/templates/ipsec.secrets.j2 similarity index 100% rename from roles/vpn/templates/ipsec.secrets.j2 rename to roles/strongswan/templates/ipsec.secrets.j2 diff --git a/roles/vpn/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 similarity index 100% rename from roles/vpn/templates/mobileconfig.j2 rename to roles/strongswan/templates/mobileconfig.j2 diff --git a/roles/vpn/templates/openssl.cnf.j2 b/roles/strongswan/templates/openssl.cnf.j2 similarity index 100% rename from roles/vpn/templates/openssl.cnf.j2 rename to roles/strongswan/templates/openssl.cnf.j2 diff --git a/roles/vpn/templates/strongswan.conf.j2 b/roles/strongswan/templates/strongswan.conf.j2 similarity index 100% rename from roles/vpn/templates/strongswan.conf.j2 rename to roles/strongswan/templates/strongswan.conf.j2 diff --git a/roles/vpn/tasks/distribute_keys.yml b/roles/vpn/tasks/distribute_keys.yml deleted file mode 100644 index d50ecfa4..00000000 --- a/roles/vpn/tasks/distribute_keys.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- - -- name: Copy the keys to the strongswan directory - copy: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - owner: "{{ item.owner }}" - group: "{{ item.group }}" - mode: "{{ item.mode }}" - with_items: - - src: "configs/{{ IP_subject_alt_name }}/pki/cacert.pem" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/cacerts/ca.crt" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - - src: "configs/{{ IP_subject_alt_name }}/pki/certs/{{ IP_subject_alt_name }}.crt" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/certs/{{ IP_subject_alt_name }}.crt" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - - src: "configs/{{ IP_subject_alt_name }}/pki/private/{{ IP_subject_alt_name }}.key" - dest: "{{ config_prefix|default('/') }}etc/ipsec.d/private/{{ IP_subject_alt_name }}.key" - owner: strongswan - group: "{{ root_group|default('root') }}" - mode: "0600" - notify: - - restart strongswan diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml index 7961c6a9..d0083366 100644 --- a/roles/wireguard/defaults/main.yml +++ b/roles/wireguard/defaults/main.yml @@ -1,4 +1,28 @@ --- wireguard_PersistentKeepalive: 0 -wireguard_client_ip: "{{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" -wireguard_server_ip: "{{ wireguard_network_ipv4['gateway'] }}/{{ wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ wireguard_network_ipv6['gateway'] }}/{{ wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_config_path: "configs/{{ IP_subject_alt_name }}/wireguard/" +wireguard_pki_path: "{{ wireguard_config_path }}/.pki/" +wireguard_interface: wg0 +_wireguard_network_ipv4: + subnet: 10.19.49.0 + prefix: 24 + gateway: 10.19.49.1 + clients_range: 10.19.49 + clients_start: 2 +_wireguard_network_ipv6: + subnet: 'fd9d:bc11:4021::' + prefix: 48 + gateway: 'fd9d:bc11:4021::1' + clients_range: 'fd9d:bc11:4021::' + clients_start: 2 +wireguard_network_ipv4: "{{ _wireguard_network_ipv4['subnet'] }}/{{ _wireguard_network_ipv4['prefix'] }}" +wireguard_network_ipv6: "{{ _wireguard_network_ipv6['subnet'] }}/{{ _wireguard_network_ipv6['prefix'] }}" +keys_clean_all: false +wireguard_dns_servers: >- + {% if local_dns|default(false)|bool or dns_encryption|default(false)|bool == true %} + {{ local_service_ip }} + {% else %} + {% for host in dns_servers.ipv4 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% if ipv6_support %},{% for host in dns_servers.ipv6 %}{{ host }}{% if not loop.last %},{% endif %}{% endfor %}{% endif %} + {% endif %} +wireguard_client_ip: "{{ _wireguard_network_ipv4['clients_range'] }}.{{ _wireguard_network_ipv4['clients_start'] + index|int + 1 }}/{{ _wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ _wireguard_network_ipv6['clients_range'] }}{{ _wireguard_network_ipv6['clients_start'] + index|int + 1 }}/{{ _wireguard_network_ipv6['prefix'] }}{% endif %}" +wireguard_server_ip: "{{ _wireguard_network_ipv4['gateway'] }}/{{ _wireguard_network_ipv4['prefix'] }}{% if ipv6_support %},{{ _wireguard_network_ipv6['gateway'] }}/{{ _wireguard_network_ipv6['prefix'] }}{% endif %}" diff --git a/roles/wireguard/tasks/keys.yml b/roles/wireguard/tasks/keys.yml index 33434081..e13a015a 100644 --- a/roles/wireguard/tasks/keys.yml +++ b/roles/wireguard/tasks/keys.yml @@ -20,7 +20,7 @@ - block: - name: Save private keys copy: - dest: "{{ wireguard_config_path }}/private/{{ item['item'] }}" + dest: "{{ wireguard_pki_path }}/private/{{ item['item'] }}" content: "{{ item['stdout'] }}" mode: "0600" no_log: true @@ -39,7 +39,7 @@ when: wg_genkey.changed - name: Generate public keys - shell: echo "{{ lookup('file', wireguard_config_path + '/private/' + item) }}" | wg pubkey + shell: echo "{{ lookup('file', wireguard_pki_path + '/private/' + item) }}" | wg pubkey register: wg_pubkey changed_when: false args: @@ -50,7 +50,7 @@ - name: Save public keys copy: - dest: "{{ wireguard_config_path }}/public/{{ item['item'] }}" + dest: "{{ wireguard_pki_path }}/public/{{ item['item'] }}" content: "{{ item['stdout'] }}" mode: "0600" no_log: true diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml index fa184fdc..235eaa45 100644 --- a/roles/wireguard/tasks/main.yml +++ b/roles/wireguard/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Ensure the required directories exist file: - dest: "{{ wireguard_config_path }}/{{ item }}" + dest: "{{ wireguard_pki_path }}/{{ item }}" state: directory recurse: true with_items: @@ -28,7 +28,7 @@ - block: - name: WireGuard user list updated lineinfile: - dest: "{{ wireguard_config_path }}/index.txt" + dest: "{{ wireguard_pki_path }}/index.txt" create: true mode: "0600" insertafter: EOF @@ -37,7 +37,7 @@ with_items: "{{ users }}" - set_fact: - wireguard_users: "{{ (lookup('file', wireguard_config_path + 'index.txt')).split('\n') }}" + wireguard_users: "{{ (lookup('file', wireguard_pki_path + 'index.txt')).split('\n') }}" - name: WireGuard users config generated template: diff --git a/roles/wireguard/templates/client.conf.j2 b/roles/wireguard/templates/client.conf.j2 index b601abb5..4c2fc62a 100644 --- a/roles/wireguard/templates/client.conf.j2 +++ b/roles/wireguard/templates/client.conf.j2 @@ -1,12 +1,12 @@ [Interface] -PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + item.1) }} +PrivateKey = {{ lookup('file', wireguard_pki_path + '/private/' + item.1) }} Address = {{ wireguard_client_ip }} DNS = {{ wireguard_dns_servers }} {% if reduce_mtu|int > 0 %}MTU = {{ 1420 - reduce_mtu|int }} {% endif %} [Peer] -PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + IP_subject_alt_name) }} +PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + IP_subject_alt_name) }} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = {{ IP_subject_alt_name }}:{{ wireguard_port }} {{ 'PersistentKeepalive = ' + wireguard_PersistentKeepalive|string if wireguard_PersistentKeepalive > 0 else '' }} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 index eb77f13a..247c7d2f 100644 --- a/roles/wireguard/templates/server.conf.j2 +++ b/roles/wireguard/templates/server.conf.j2 @@ -1,7 +1,7 @@ [Interface] Address = {{ wireguard_server_ip }} ListenPort = {{ wireguard_port }} -PrivateKey = {{ lookup('file', wireguard_config_path + '/private/' + IP_subject_alt_name) }} +PrivateKey = {{ lookup('file', wireguard_pki_path + '/private/' + IP_subject_alt_name) }} SaveConfig = false {% for u in wireguard_users %} @@ -10,8 +10,8 @@ SaveConfig = false [Peer] # {{ u }} -PublicKey = {{ lookup('file', wireguard_config_path + '/public/' + u) }} -AllowedIPs = {{ wireguard_network_ipv4['clients_range'] }}.{{ wireguard_network_ipv4['clients_start'] + index }}/32{% if ipv6_support %},{{ wireguard_network_ipv6['clients_range'] }}{{ wireguard_network_ipv6['clients_start'] + index }}/128{% endif %} +PublicKey = {{ lookup('file', wireguard_pki_path + '/public/' + u) }} +AllowedIPs = {{ _wireguard_network_ipv4['clients_range'] }}.{{ _wireguard_network_ipv4['clients_start'] + index }}/32{% if ipv6_support %},{{ _wireguard_network_ipv6['clients_range'] }}{{ _wireguard_network_ipv6['clients_start'] + index }}/128{% endif %} {% endif %} {% endfor %} diff --git a/server.yml b/server.yml index b6e8340b..1ab38346 100644 --- a/server.yml +++ b/server.yml @@ -19,8 +19,9 @@ - role: wireguard when: wireguard_enabled tags: wireguard - - role: vpn - tags: vpn + - role: strongswan + when: ipsec_enabled + tags: ipsec - role: ssh_tunneling when: algo_ssh_tunneling tags: ssh_tunneling @@ -30,15 +31,17 @@ - name: Delete the CA key local_action: module: file - path: "configs/{{ IP_subject_alt_name }}/pki/private/cakey.pem" + path: "{{ ipsec_pki_path }}/private/cakey.pem" state: absent become: false - when: not algo_store_cakey + when: + - ipsec_enabled + - not algo_store_cakey - name: Dump the configuration local_action: module: copy - dest: "configs/{{ IP_subject_alt_name }}/config.yml" + dest: "configs/{{ IP_subject_alt_name }}/.config.yml" content: | server: {{ 'localhost' if inventory_hostname == 'localhost' else inventory_hostname }} server_user: {{ ansible_ssh_user }} @@ -55,6 +58,8 @@ algo_windows: {{ algo_windows }} algo_store_cakey: {{ algo_store_cakey }} IP_subject_alt_name: {{ IP_subject_alt_name }} + ipsec_enabled: {{ ipsec_enabled }} + wireguard_enabled: {{ wireguard_enabled }} {% if tests|default(false)|bool %}ca_password: {{ CA_password }}{% endif %} become: false @@ -69,9 +74,9 @@ - debug: msg: - "{{ congrats.common.split('\n') }}" - - " {{ congrats.p12_pass }}" - - " {% if algo_store_cakey %}{{ congrats.ca_key_pass }}{% endif %}" - - " {% if algo_provider != 'local' %}{{ congrats.ssh_access }}{% endif %}" + - " {{ congrats.p12_pass if algo_ssh_tunneling or ipsec_enabled else '' }}" + - " {{ congrats.ca_key_pass if algo_store_cakey and ipsec_enabled else '' }}" + - " {{ congrats.ssh_access if algo_provider != 'local' else ''}}" tags: always rescue: - debug: var=fail_hint diff --git a/tests/update-users.sh b/tests/update-users.sh index 2387103f..c083994c 100755 --- a/tests/update-users.sh +++ b/tests/update-users.sh @@ -11,7 +11,11 @@ else ansible-playbook users.yml -e "${USER_ARGS}" -t update-users fi -if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.crt | grep CRL +# +# IPsec +# + +if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/crl/phone.crt | grep CRL then echo "The CRL check passed" else @@ -19,10 +23,34 @@ if sudo openssl crl -inform pem -noout -text -in configs/$LXC_IP/pki/crl/phone.c exit 1 fi -if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/pki/certs/user1.crt | grep CN=user1 +if sudo openssl x509 -inform pem -noout -text -in configs/$LXC_IP/ipsec/.pki/certs/user1.crt | grep CN=user1 then echo "The new user exists" else echo "The new user does not exist" exit 1 fi + +# +# WireGuard +# + +if sudo test -f configs/$LXC_IP/wireguard/user1.conf + then + echo "WireGuard: The new user exists" + else + echo "WireGuard: The new user does not exist" + exit 1 +fi + +# +# SSH tunneling +# + +if sudo test -f configs/$LXC_IP/ssh-tunnel/user1.ssh_config + then + echo "SSH Tunneling: The new user exists" + else + echo "SSH Tunneling: The new user does not exist" + exit 1 +fi diff --git a/users.yml b/users.yml index 64422638..3f742946 100644 --- a/users.yml +++ b/users.yml @@ -21,13 +21,15 @@ - name: Import host specific variables include_vars: - file: "configs/{{ algo_server }}/config.yml" + file: "configs/{{ algo_server }}/.config.yml" - pause: prompt: Enter the password for the private CA key echo: false register: _ca_password - when: ca_password is undefined + when: + - ca_password is undefined + - ipsec_enabled - name: Set facts based on the input set_fact: @@ -42,7 +44,7 @@ groups: vpn-host ansible_ssh_user: "{{ server_user|default('root') }}" ansible_connection: "{% if algo_server == 'localhost' %}local{% else %}ssh{% endif %}" - ansible_python_interpreter: "/usr/bin/python2.7" + ansible_python_interpreter: "/usr/bin/python3" CA_password: "{{ CA_password }}" rescue: - debug: var=fail_hint @@ -56,7 +58,7 @@ become: true vars_files: - config.cfg - - "configs/{{ inventory_hostname }}/config.yml" + - "configs/{{ inventory_hostname }}/.config.yml" pre_tasks: - block: @@ -74,8 +76,9 @@ - role: wireguard tags: [ 'vpn', 'wireguard' ] when: wireguard_enabled - - role: vpn - tags: vpn + - role: strongswan + when: ipsec_enabled + tags: ipsec - role: ssh_tunneling when: algo_ssh_tunneling From 9b89801b8ac38a6b9631a8e7eab25a4ed2580bd3 Mon Sep 17 00:00:00 2001 From: Les Aker Date: Mon, 11 Mar 2019 15:29:39 +0300 Subject: [PATCH 761/769] skip generation of SSH keypair when deploying locally (#1348) --- playbooks/cloud-pre.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playbooks/cloud-pre.yml b/playbooks/cloud-pre.yml index 6a8071b0..53de7fab 100644 --- a/playbooks/cloud-pre.yml +++ b/playbooks/cloud-pre.yml @@ -31,9 +31,11 @@ size: 2048 mode: "0600" type: RSA + when: algo_provider != "local" - name: Generate the SSH public key openssl_publickey: path: "{{ SSH_keys.public }}" privatekey_path: "{{ SSH_keys.private }}" format: OpenSSH + when: algo_provider != "local" From 3428c5197eb80b93a214f179d46518001e20811e Mon Sep 17 00:00:00 2001 From: Ryan Kasper Date: Mon, 11 Mar 2019 20:08:09 -0600 Subject: [PATCH 762/769] Fix typo in doctl command (#1350) --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 493661cf..5bd0f84a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -215,7 +215,7 @@ The error is caused because Digital Ocean changed its API to treat the tag argum 1. Download [doctl](https://github.com/digitalocean/doctl) 2. Run `doctl auth init`; it will ask you for your token which you can get (or generate) on the API tab at DigitalOcean 3. Once you are authorized on DO, you can run `doctl compute tag list` to see the list of tags -4. Run `doctl compute tag delete enivronment:algo --force` to delete the environment:algo tag +4. Run `doctl compute tag delete environment:algo --force` to delete the environment:algo tag 5. Finally run `doctl compute tag list` to make sure that the tag has been deleted 6. Run algo as directed From 4ae5972f9468d79397f5ee910df8008c648db567 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Thu, 14 Mar 2019 18:11:57 +0100 Subject: [PATCH 763/769] Start dnscrypt-proxy after systemd-resolved (#1357) --- roles/dns_encryption/tasks/ubuntu.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/roles/dns_encryption/tasks/ubuntu.yml b/roles/dns_encryption/tasks/ubuntu.yml index 89515ddb..f9cd7ee0 100644 --- a/roles/dns_encryption/tasks/ubuntu.yml +++ b/roles/dns_encryption/tasks/ubuntu.yml @@ -47,10 +47,14 @@ owner: root group: root -- name: Ubuntu | Add capabilities to bind ports +- name: Ubuntu | Add custom requirements to successfully start the unit copy: - dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-capabilities.conf + dest: /etc/systemd/system/dnscrypt-proxy.service.d/99-algo.conf content: | + [Unit] + After=systemd-resolved.service + Requires=systemd-resolved.service + [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE notify: From 30beadb9490342b1da9ca0fed187720adc71d0d1 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Fri, 15 Mar 2019 18:16:26 +0100 Subject: [PATCH 764/769] Modify naming in the cloud resources and client config files (#1353) * Modify naming in the cloud resources and client config files * Azure template: Eliminate unneeded variables --- roles/cloud-azure/files/deployment.json | 50 ++++++++----------- roles/cloud-azure/tasks/main.yml | 9 ++-- roles/cloud-ec2/files/stack.yml | 28 +++-------- roles/cloud-gce/tasks/main.yml | 6 +-- .../templates/client_windows.ps1.j2 | 2 +- roles/strongswan/templates/mobileconfig.j2 | 12 ++--- 6 files changed, 43 insertions(+), 64 deletions(-) diff --git a/roles/cloud-azure/files/deployment.json b/roles/cloud-azure/files/deployment.json index 646ea8a1..027e562b 100644 --- a/roles/cloud-azure/files/deployment.json +++ b/roles/cloud-azure/files/deployment.json @@ -2,15 +2,9 @@ "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json", "contentVersion": "1.0.0.0", "parameters": { - "AlgoServerName": { - "type": "string" - }, "sshKeyData": { "type": "string" }, - "location": { - "type": "string" - }, "WireGuardPort": { "type": "int" }, @@ -22,15 +16,15 @@ } }, "variables": { - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('AlgoServerName'))]", - "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', parameters('AlgoServerName'))]" + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks', resourceGroup().name)]", + "subnet1Ref": "[concat(variables('vnetID'),'/subnets/', resourceGroup().name)]" }, "resources": [ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/networkSecurityGroups", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "securityRules": [ { @@ -95,8 +89,8 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/publicIPAddresses", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "publicIPAllocationMethod": "Static" } @@ -104,8 +98,8 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/virtualNetworks", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "properties": { "addressSpace": { "addressPrefixes": [ @@ -114,7 +108,7 @@ }, "subnets": [ { - "name": "[parameters('AlgoServerName')]", + "name": "[resourceGroup().name]", "properties": { "addressPrefix": "10.10.0.0/24" } @@ -125,16 +119,16 @@ { "apiVersion": "2015-06-15", "type": "Microsoft.Network/networkInterfaces", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "dependsOn": [ - "[concat('Microsoft.Network/networkSecurityGroups/', parameters('AlgoServerName'))]", - "[concat('Microsoft.Network/publicIPAddresses/', parameters('AlgoServerName'))]", - "[concat('Microsoft.Network/virtualNetworks/', parameters('AlgoServerName'))]" + "[concat('Microsoft.Network/networkSecurityGroups/', resourceGroup().name)]", + "[concat('Microsoft.Network/publicIPAddresses/', resourceGroup().name)]", + "[concat('Microsoft.Network/virtualNetworks/', resourceGroup().name)]" ], "properties": { "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/networkSecurityGroups', resourceGroup().name)]" }, "ipConfigurations": [ { @@ -142,7 +136,7 @@ "properties": { "privateIPAllocationMethod": "Dynamic", "publicIPAddress": { - "id": "[resourceId('Microsoft.Network/publicIPAddresses', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/publicIPAddresses', resourceGroup().name)]" }, "subnet": { "id": "[variables('subnet1Ref')]" @@ -155,17 +149,17 @@ { "apiVersion": "2016-04-30-preview", "type": "Microsoft.Compute/virtualMachines", - "name": "[parameters('AlgoServerName')]", - "location": "[parameters('location')]", + "name": "[resourceGroup().name]", + "location": "[resourceGroup().location]", "dependsOn": [ - "[concat('Microsoft.Network/networkInterfaces/', parameters('AlgoServerName'))]" + "[concat('Microsoft.Network/networkInterfaces/', resourceGroup().name)]" ], "properties": { "hardwareProfile": { "vmSize": "[parameters('vmSize')]" }, "osProfile": { - "computerName": "[parameters('AlgoServerName')]", + "computerName": "[resourceGroup().name]", "adminUsername": "ubuntu", "linuxConfiguration": { "disablePasswordAuthentication": true, @@ -193,7 +187,7 @@ "networkProfile": { "networkInterfaces": [ { - "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('AlgoServerName'))]" + "id": "[resourceId('Microsoft.Network/networkInterfaces', resourceGroup().name)]" } ] } @@ -203,7 +197,7 @@ "outputs": { "publicIPAddresses": { "type": "string", - "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',parameters('AlgoServerName')),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", + "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses',resourceGroup().name),providers('Microsoft.Network', 'publicIPAddresses').apiVersions[0]).ipAddress]", } } } diff --git a/roles/cloud-azure/tasks/main.yml b/roles/cloud-azure/tasks/main.yml index 38adc741..113352ce 100644 --- a/roles/cloud-azure/tasks/main.yml +++ b/roles/cloud-azure/tasks/main.yml @@ -16,20 +16,17 @@ - name: Create AlgoVPN Server azure_rm_deployment: state: present - deployment_name: "AlgoVPN-{{ algo_server_name }}" + deployment_name: "{{ algo_server_name }}" template: "{{ lookup('file', 'deployment.json') }}" secret: "{{ secret }}" tenant: "{{ tenant }}" client_id: "{{ client_id }}" subscription_id: "{{ subscription_id }}" - resource_group_name: "AlgoVPN-{{ algo_server_name }}" + resource_group_name: "{{ algo_server_name }}" + location: "{{ algo_region }}" parameters: - AlgoServerName: - value: "{{ algo_server_name }}" sshKeyData: value: "{{ lookup('file', '{{ SSH_keys.public }}') }}" - location: - value: "{{ algo_region }}" WireGuardPort: value: "{{ wireguard_port }}" vmSize: diff --git a/roles/cloud-ec2/files/stack.yml b/roles/cloud-ec2/files/stack.yml index 3660613b..829a2cb3 100644 --- a/roles/cloud-ec2/files/stack.yml +++ b/roles/cloud-ec2/files/stack.yml @@ -21,9 +21,7 @@ Resources: InstanceTenancy: default Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName VPCIPv6: Type: AWS::EC2::VPCCidrBlock @@ -35,22 +33,18 @@ Resources: Type: AWS::EC2::InternetGateway Properties: Tags: - - Key: Environment - Value: Algo - Key: Name - Value: Algo + Value: !Ref AWS::StackName Subnet: Type: AWS::EC2::Subnet Properties: CidrBlock: 172.16.254.0/23 MapPublicIpOnLaunch: false - Tags: - - Key: Environment - Value: Algo - - Key: Name - Value: Algo VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Ref AWS::StackName VPCGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment @@ -63,10 +57,8 @@ Resources: Properties: VpcId: !Ref VPC Tags: - - Key: Environment - Value: Algo - Key: Name - Value: Algo + Value: !Ref AWS::StackName Route: Type: AWS::EC2::Route @@ -140,9 +132,7 @@ Resources: CidrIp: 0.0.0.0/0 Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName EC2Instance: Type: AWS::EC2::Instance @@ -181,9 +171,7 @@ Resources: cfn-signal -e $? --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} Tags: - Key: Name - Value: Algo - - Key: Environment - Value: Algo + Value: !Ref AWS::StackName ElasticIP: Type: AWS::EC2::EIP diff --git a/roles/cloud-gce/tasks/main.yml b/roles/cloud-gce/tasks/main.yml index e04b3d80..baa5f469 100644 --- a/roles/cloud-gce/tasks/main.yml +++ b/roles/cloud-gce/tasks/main.yml @@ -8,8 +8,8 @@ - name: Network configured gce_net: - name: "algo-net-{{ algo_server_name }}" - fwname: "algo-net-{{ algo_server_name }}-fw" + name: "{{ algo_server_name }}" + fwname: "{{ algo_server_name }}-fw" allowed: "udp:500,4500,{{ wireguard_port }};tcp:22" state: "present" mode: auto @@ -45,7 +45,7 @@ credentials_file: "{{ credentials_file_path }}" project_id: "{{ project_id }}" metadata: '{"ssh-keys":"ubuntu:{{ ssh_public_key_lookup }}"}' - network: "algo-net-{{ algo_server_name }}" + network: "{{ algo_server_name }}" tags: - "environment-algo" register: google_vm diff --git a/roles/strongswan/templates/client_windows.ps1.j2 b/roles/strongswan/templates/client_windows.ps1.j2 index e1021bbe..da53383f 100644 --- a/roles/strongswan/templates/client_windows.ps1.j2 +++ b/roles/strongswan/templates/client_windows.ps1.j2 @@ -85,7 +85,7 @@ Save the embedded CA cert and encrypted user PKCS12 file. $ErrorActionPreference = "Stop" $VpnServerAddress = "{{ IP_subject_alt_name }}" -$VpnName = "Algo VPN {{ IP_subject_alt_name }} IKEv2" +$VpnName = "AlgoVPN {{ algo_server_name }} IKEv2" $VpnUser = "{{ item.0 }}" $CaCertificateBase64 = "{{ PayloadContentCA }}" $UserPkcs12Base64 = "{{ item.1.stdout }}" diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index 686ed7e8..e9d66701 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -116,7 +116,7 @@ PayloadDescription Configures VPN settings PayloadDisplayName - VPN + {{ algo_server_name }} PayloadIdentifier com.apple.vpn.managed.{{ VPN_PayloadIdentifier }} PayloadType @@ -133,7 +133,7 @@ 0 UserDefinedName - Algo VPN {{ IP_subject_alt_name }} IKEv2 + AlgoVPN {{ algo_server_name }} IKEv2 VPNType IKEv2 @@ -149,7 +149,7 @@ PayloadDescription Adds a PKCS#12-formatted certificate PayloadDisplayName - {{ item.0 }}.p12 + {{ algo_server_name }} PayloadIdentifier com.apple.security.pkcs12.{{ pkcs12_PayloadCertificateUUID }} PayloadType @@ -169,7 +169,7 @@ PayloadDescription Adds a CA root certificate PayloadDisplayName - {{ IP_subject_alt_name }} + {{ algo_server_name }} PayloadIdentifier com.apple.security.root.{{ CA_PayloadIdentifier }} PayloadType @@ -181,11 +181,11 @@ PayloadDisplayName - {{ IP_subject_alt_name }} IKEv2 + AlgoVPN {{ algo_server_name }} IKEv2 PayloadIdentifier donut.local.{{ 500000 | random | to_uuid | upper }} PayloadOrganization - Algo VPN + AlgoVPN PayloadRemovalDisallowed PayloadType From db34d55b78111766904cb71e0c114ca40b7fd5ee Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Sun, 17 Mar 2019 11:19:24 -0400 Subject: [PATCH 765/769] AGPLv3 change (#1351) --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 2 + 2 files changed, 659 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 7365a69d..bae94e18 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,661 @@ -The MIT License (MIT) + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2016 Trail of Bits + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md index 2308c31c..c4016c85 100644 --- a/README.md +++ b/README.md @@ -246,3 +246,5 @@ All donations support continued development. Thanks! * We accept donations via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CYZZD39GXUJ3E), [Patreon](https://www.patreon.com/algovpn), and [Flattr](https://flattr.com/submit/auto?fid=kxw60j&url=https%3A%2F%2Fgithub.com%2Ftrailofbits%2Falgo). * Use our [referral code](https://m.do.co/c/4d7f4ff9cfe4) when you sign up to Digital Ocean for a $10 credit. * We also accept and appreciate contributions of new code and bugfixes via Github Pull Requests. + +Algo is licensed and distributed under the AGPLv3. If you want to distribute a closed-source modification or service based on Algo, then please consider purchasing an exception . As with the methods above, this will help support continued development. \ No newline at end of file From 58ce62e2bd690f5f3192471e0160ddb62b82df71 Mon Sep 17 00:00:00 2001 From: Jack Ivanov <17044561+jackivanov@users.noreply.github.com> Date: Tue, 19 Mar 2019 08:57:05 +0100 Subject: [PATCH 766/769] Update CHANGELOG.md --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd579d..843d8ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.1 (Unreleased) + +### Added + +### Fixed + +## 1.0 (Mar 19, 2019) + +### Added +- Tagged releases and changelog [\#724](https://github.com/trailofbits/algo/issues/724) +- Add support for custom domain names [\#759](https://github.com/trailofbits/algo/issues/759) + +### Fixed +- Set the name shown to the user \(client\) to be the server name specified in the install script [\#491](https://github.com/trailofbits/algo/issues/491) +- AGPLv3 change [\#1351](https://github.com/trailofbits/algo/pull/1351) +- Migrate to python3 [\#1024](https://github.com/trailofbits/algo/issues/1024) +- Reorganize the project around ipsec + wireguard [\#1330](https://github.com/trailofbits/algo/issues/1330) +- Configuration folder reorganization [\#1330](https://github.com/trailofbits/algo/issues/1330) +- Remove WireGuard KeepAlive and include as an option in config [\#1251](https://github.com/trailofbits/algo/issues/1251) +- Dnscrypt-proxy no longer works after reboot [\#1356](https://github.com/trailofbits/algo/issues/1356) + ## 20 Oct 2018 ### Added - AWS Lightsail From 13c4628b5d553bbccf8be6b1aca1d1e5a9e8f805 Mon Sep 17 00:00:00 2001 From: Fabian Foerg <3429782+faf0@users.noreply.github.com> Date: Tue, 19 Mar 2019 09:49:18 -0700 Subject: [PATCH 767/769] Simplify Apple Profile Configuration Template (#1033) * Simplify Apple Profile Configuration Template * enable lstrip_blocks * remove ldashes --- roles/strongswan/templates/mobileconfig.j2 | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/roles/strongswan/templates/mobileconfig.j2 b/roles/strongswan/templates/mobileconfig.j2 index e9d66701..6cf0ea13 100644 --- a/roles/strongswan/templates/mobileconfig.j2 +++ b/roles/strongswan/templates/mobileconfig.j2 @@ -1,3 +1,4 @@ +#jinja2:lstrip_blocks: True @@ -12,8 +13,8 @@ 1 OnDemandRules -{% if algo_ondemand_wifi_exclude|b64decode != '_null' %} -{% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} + {% if algo_ondemand_wifi_exclude|b64decode != '_null' %} + {% set WIFI_EXCLUDE_LIST = (algo_ondemand_wifi_exclude|b64decode|string).split(',') %} Action Disconnect @@ -21,20 +22,19 @@ WiFi SSIDMatch -{% for network_name in WIFI_EXCLUDE_LIST %} + {% for network_name in WIFI_EXCLUDE_LIST %} {{ network_name|e }} -{% endfor %} + {% endfor %} -{% else %} -{% endif %} + {% endif %} Action -{% if algo_ondemand_wifi %} + {% if algo_ondemand_wifi %} Connect {% else %} Disconnect -{% endif %} + {% endif %} InterfaceTypeMatch WiFi URLStringProbe @@ -42,11 +42,11 @@ Action -{% if algo_ondemand_cellular %} + {% if algo_ondemand_cellular %} Connect {% else %} Disconnect -{% endif %} + {% endif %} InterfaceTypeMatch Cellular URLStringProbe @@ -57,7 +57,6 @@ Disconnect -{% else %} {% endif %} AuthenticationMethod Certificate From d996b1d02f966f8bd64bf4cc9f37558a4b70872b Mon Sep 17 00:00:00 2001 From: adamluk Date: Mon, 25 Mar 2019 07:55:38 +0000 Subject: [PATCH 768/769] Update 10-algo-lo100.network.j2 (#1369) --- roles/common/templates/10-algo-lo100.network.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/common/templates/10-algo-lo100.network.j2 b/roles/common/templates/10-algo-lo100.network.j2 index 257396c6..87280511 100644 --- a/roles/common/templates/10-algo-lo100.network.j2 +++ b/roles/common/templates/10-algo-lo100.network.j2 @@ -2,6 +2,6 @@ Name=lo [Network] -Label=lo:100 +Description=lo:100 Address={{ local_service_ip }}/32 Address=FCAA::1/64 From 02dd6ac457f67a4f1a6851e886848f952fca4695 Mon Sep 17 00:00:00 2001 From: tc1977 Date: Tue, 26 Mar 2019 13:12:04 -0400 Subject: [PATCH 769/769] Update ipsec_configuration and add custom charon.conf --- .../strongswan/tasks/ipsec_configuration.yml | 5 + roles/strongswan/templates/charon.conf.j2 | 366 ++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 roles/strongswan/templates/charon.conf.j2 diff --git a/roles/strongswan/tasks/ipsec_configuration.yml b/roles/strongswan/tasks/ipsec_configuration.yml index ce5b3e5b..28d2f9ff 100644 --- a/roles/strongswan/tasks/ipsec_configuration.yml +++ b/roles/strongswan/tasks/ipsec_configuration.yml @@ -23,6 +23,11 @@ owner: strongswan group: "{{ root_group|default('root') }}" mode: "0600" + - src: charon.conf.j2 + dest: "strongswan.d/charon.conf" + owner: root + group: "{{root_group|default('root') }}" + mode: "0644" notify: - restart strongswan diff --git a/roles/strongswan/templates/charon.conf.j2 b/roles/strongswan/templates/charon.conf.j2 new file mode 100644 index 00000000..4206e056 --- /dev/null +++ b/roles/strongswan/templates/charon.conf.j2 @@ -0,0 +1,366 @@ +# 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 Certificate Revocation Lists (CRLs) fetched via HTTP or LDAP + # should be saved under a unique file name derived from the public key of + # the Certification Authority (CA) to /etc/ipsec.d/crls (stroke) or + # /etc/swanctl/x509crl (vici), respectively. + # cache_crls = no + + # 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 = yes + + # Number of half-open IKE_SAs that activate the cookie mechanism. + # cookie_threshold = 10 + + # Delete CHILD_SAs right after they got successfully rekeyed (IKEv1 only). + # delete_rekeyed = no + + # Delay in seconds until inbound IPsec SAs are deleted after rekeyings + # (IKEv2 only). + # delete_rekeyed_delay = 5 + + # Use ANSI X9.42 DH exponent size or optimum size matched to cryptographic + # strength. + # dh_exponent_ansi_x9_42 = yes + + # Use RTLD_NOW with dlopen when loading plugins and IMV/IMCs to reveal + # missing symbols immediately. + # dlopen_use_rtld_now = no + + # 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 + + # Whether to follow IKEv2 redirects (RFC 5685). + # follow_redirects = yes + + # Maximum size (complete IP datagram size in bytes) of a sent IKE fragment + # when using proprietary IKEv1 or standardized IKEv2 fragmentation, defaults + # to 1280 (use 0 for address family specific default values, which uses a + # lower value for IPv4). If specified this limit is used for both IPv4 and + # IPv6. + # fragment_size = 1280 + + # 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 = 5 + + # 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 + + # Whether to ignore the traffic selectors from the kernel's acquire events + # for IKEv2 connections (they are not used for IKEv1). + # ignore_acquire_ts = 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 = yes + + # Install virtual IP addresses. + # install_virtual_ip = yes + + # 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 = 25s + + # Plugins to load in the IKE daemon charon. + # load = + + # Determine plugins to load via each plugin's load option. + # load_modular = no + + # Initiate IKEv2 reauthentication with a make-before-break scheme. + # make_before_break = no + + # Maximum number of IKEv1 phase 2 exchanges per IKE_SA to keep state about + # and track concurrently. + # max_ikev1_exchanges = 3 + + # 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 + + # Whether to prefer updating SAs to the path with the best route. + # prefer_best_path = no + + # Prefer locally configured proposals for IKE/IPsec over supplied ones as + # responder (disabling this can avoid keying retries due to + # INVALID_KE_PAYLOAD notifies). + # prefer_configured_proposals = yes + + # 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 + + # Maximum jitter in percent to apply randomly to calculated retransmission + # timeout (0 to disable). + # retransmit_jitter = 0 + + # Upper limit in seconds for calculated retransmission timeout (0 to + # disable). + # retransmit_limit = 0 + + # 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 in seconds 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 (always enabled for IKEv1). + reuse_ikesa = yes + + # Numerical routing table to install routes to. + # routing_table = + + # Priority of the routing table. + # routing_table_prio = + + # Whether to use RSA with PSS padding instead of PKCS#1 padding by default. + # rsa_pss = no + + # 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 + + # Whether to enable Signature Authentication as per RFC 7427. + # signature_authentication = yes + + # Whether to enable constraints against IKEv2 signature schemes. + # signature_authentication_constraints = yes + + # The upper limit for SPIs requested from the kernel for IPsec SAs. + # spi_max = 0xcfffffff + + # The lower limit for SPIs requested from the kernel for IPsec SAs. + # spi_min = 0xc0000000 + + # 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 + + } + +} +

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%0DYOv&xKwGhV<#qG+Af`v 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