diff --git a/playbooks/roles/tor-bridge/files/system_tor_hs b/playbooks/roles/tor-bridge/files/system_tor_hs new file mode 100644 index 000000000..686ca905a --- /dev/null +++ b/playbooks/roles/tor-bridge/files/system_tor_hs @@ -0,0 +1,22 @@ +# vim:syntax=apparmor +#include + +profile system_tor_hs flags=(attach_disconnected) { + #include + + owner /var/lib/tor-hidden-service/** rwk, + owner /var/lib/tor-hidden-service/ r, + owner /var/log/tor-hidden-service/* w, + + # During startup, tor (as root) tries to open various things such as + # directories via check_private_dir(). Let it. + /var/lib/tor-hidden-service/** r, + + /{,var/}run/tor-hidden-service/ r, + /{,var/}run/tor-hidden-service/control w, + /{,var/}run/tor-hidden-service/socks w, + /{,var/}run/tor-hidden-service/tor.pid w, + /{,var/}run/tor-hidden-service/control.authcookie w, + /{,var/}run/tor-hidden-service/control.authcookie.tmp rw, + /{,var/}run/systemd/notify w, +} diff --git a/playbooks/roles/tor-bridge/tasks/main.yml b/playbooks/roles/tor-bridge/tasks/main.yml index 75ef49b80..8c20aa9f1 100644 --- a/playbooks/roles/tor-bridge/tasks/main.yml +++ b/playbooks/roles/tor-bridge/tasks/main.yml @@ -16,11 +16,13 @@ apt: package: deb.torproject.org-keyring -- name: Install obfs4 and Tor +- name: Install obfs4, Tor, and virtualenv apt: package: - obfs4proxy - tor + # Vanguards dependency + - python-virtualenv # Update the firewall to allow Tor and obfs4proxy # NOTE(@cpu): we do this early in the role because the Tor daemon will check if @@ -46,6 +48,145 @@ group: root mode: 0644 +- name: Generate the hidden service torrc config file + template: + src: torrc-hidden-service.j2 + dest: /etc/tor/torrc-hidden-service + owner: root + group: root + mode: 0644 + +- name: Generate the vanguards config file + template: + src: vanguards.conf.j2 + dest: "{{ tor_vanguards_config }}" + owner: root + group: root + mode: 0644 + +# We have to setup an apparmor profile for the tor hidden service +# since we're trying to use the non-default tor data directories, +# control socket, PID file, etc. +- name: Copy Tor Hidden Service AppArmor profile + copy: + src: system_tor_hs + dest: /etc/apparmor.d/system_tor_hs + owner: root + group: root + mode: 0644 + +# Permissions are handled by the tor-hidden-service.service file +# (systemd) each time the service is started/restarted. +- name: Create hidden service data directory + file: + path: "{{ tor_hidden_service_state_directory }}" + state: directory + +- name: Generate the hidden service systemd file + template: + src: tor-hidden-service.service.j2 + dest: /etc/systemd/system/tor-hidden-service.service + owner: root + group: root + mode: 0644 + +- name: Generate the vanguards service systemd file + template: + src: vanguards.service.j2 + dest: /etc/systemd/system/vanguards.service + owner: root + group: root + mode: 0644 + +# Run tor processes as separate users locally. By default, +# tor runs as "debian-tor". Since we're running the hidden service +# and bridge as separate tor processes, it makes sense to run +# these under separate user accounts as well. +- name: Create local hidden service user + user: + name: "{{ tor_hidden_service_user }}" + shell: /bin/false + system: yes + create_home: False + # the "home" folder for debian-tor by default is + # set to /var/lib/tor, since we aren't using that dir + # here we set debian-tor-hs home to the new HS data dir + home: "{{ tor_hidden_service_state_directory }}" + +- name: Start tor hidden service + service: + name: tor-hidden-service + enabled: true + state: started + +# Clone the vanguards repo for hardening the Tor hidden service +# See https://blog.torproject.org/announcing-vanguards-add-onion-services +# and https://github.com/mikeperry-tor/vanguards +# Installation instructions recommend doing this via git, as deb/rpm packages +# aren't as generally available yet (checking xenial proved fruitless for example, +# it looks like vanguards is only in ubuntu's apt repos for newer releases). +- name: Clone vanguards onion services add-on + git: + repo: "{{ tor_vanguards_repo_url }}" + dest: "{{ tor_vanguards_addon_directory }}" + # Keep a separate git directory to keep failed verifications/pulls/etc. from messing + # up anything in the working tree. Just suffixed with .git in the same dir. + separate_git_dir: "{{ tor_vanguards_addon_directory }}.git" + +- name: Find latest vanguards add-on git tag + # Find latest available tag by sorting by "taggerdate" (date tag was created) + # We also want to avoid ever checking out an alpha/beta/dev tag and only use "stable" tags + shell: git tag -l --sort=-taggerdate | grep -v "alpha\|beta\|dev\|test" | head -n 1 + args: + chdir: "{{ tor_vanguards_addon_directory }}.git" + register: tor_vanguard_git_tag + +- name: Checkout latest git tag in vanguard repo + command: "git checkout {{ tor_vanguard_git_tag.stdout }}" + args: + chdir: "{{ tor_vanguards_addon_directory }}" + +# Tell git to use gpg2 when verifying tags/commits +- shell: "git config --global gpg.program $(which gpg2)" + +# Grab the signing key. +- name: Grab Vanguards add-on PGP signing key + command: "gpg2 {{ streisand_default_gpg_flags }} {{ streisand_default_key_import_flags}} --recv-keys {{ tor_vanguards_addon_gpg_key }}" + +# Now, finally, we can run the gpg verify +# We tell git (or really, gpg) where Streisand's default keyring is located by using +# the env variable $GNUPGHOME. This way, when git makes a call to gpg2 it uses the right +# keyring and can find the public key associated with the tag/commit. +- name: Verify vanguard add-on git signature + git: + repo: "{{ tor_vanguards_repo_url }}" + dest: "{{ tor_vanguards_addon_directory }}" + version: "{{ tor_vanguard_git_tag.stdout }}" + verify_commit: yes + environment: + GNUPGHOME: "{{ streisand_gpg_dir }}" + register: tor_vanguard_git_verify_results + +- name: Setup vanguards virtualenv/install + command: "{{ tor_vanguards_addon_directory }}/setup.sh" + args: + chdir: "{{ tor_vanguards_addon_directory }}" + +- name: Install cronjob to auto-update and verify vanguards + template: + src: "tor-vanguards-update-and-verify.sh.j2" + dest: "/etc/cron.daily/tor-vanguards-update-and-verify.sh" + owner: root + group: root + mode: 0755 + when: not streisand_ci + +- name: Start the vanguards service + service: + name: vanguards + enabled: true + state: started + # TODO(@cpu) - This should be removed once it isn't required, maybe in the next # release after tor 0.3.0.9 - name: Copy a local override for the Tor AppArmor profile in place diff --git a/playbooks/roles/tor-bridge/templates/tor-hidden-service.service.j2 b/playbooks/roles/tor-bridge/templates/tor-hidden-service.service.j2 new file mode 100644 index 000000000..1f3970d69 --- /dev/null +++ b/playbooks/roles/tor-bridge/templates/tor-hidden-service.service.j2 @@ -0,0 +1,37 @@ +[Unit] +Description=Anonymizing overlay network for TCP +After=network.target nss-lookup.target tor@default.service +PartOf=tor-hidden-service.service +ReloadPropagatedFrom=tor-hidden-service.service + +[Service] +Type=notify +NotifyAccess=all +PIDFile={{ tor_hidden_service_run_directory }}/tor.pid +PermissionsStartOnly=yes +ExecStartPre=/usr/bin/install -Z -m 02755 -o {{ tor_hidden_service_user }} -g {{ tor_hidden_service_user }} -d {{ tor_hidden_service_run_directory }} +ExecStartPre=/usr/bin/install -Z -m 02755 -o {{ tor_hidden_service_user }} -g {{ tor_hidden_service_user }} -d {{ tor_hidden_service_state_directory }} +ExecStartPre=/usr/bin/tor --defaults-torrc /usr/share/tor/tor-service-defaults-torrc -f {{ tor_hidden_service_config }} --RunAsDaemon 0 --verify-config +ExecStart=/usr/bin/tor --defaults-torrc /usr/share/tor/tor-service-defaults-torrc -f {{ tor_hidden_service_config }} --RunAsDaemon 0 +ExecReload=/bin/kill -HUP ${MAINPID} +KillSignal=SIGINT +TimeoutStartSec=300 +TimeoutStopSec=60 +Restart=on-failure +LimitNOFILE=65536 + +# Hardening +# system_tor_hs is a hidden service apparmor profile so we can use +# different tor data dir/control socket/PID file. +AppArmorProfile=-system_tor_hs +NoNewPrivileges=yes +PrivateTmp=yes +PrivateDevices=yes +ProtectHome=yes +ProtectSystem=full +ReadOnlyDirectories=/ +ReadWriteDirectories=-/proc +ReadWriteDirectories=-{{ tor_hidden_service_state_directory }} +ReadWriteDirectories=-{{ tor_hidden_service_log_directory }} +ReadWriteDirectories=-/run +CapabilityBoundingSet=CAP_SETUID CAP_SETGID CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH diff --git a/playbooks/roles/tor-bridge/templates/tor-vanguards-update-and-verify.sh.j2 b/playbooks/roles/tor-bridge/templates/tor-vanguards-update-and-verify.sh.j2 new file mode 100644 index 000000000..31a145878 --- /dev/null +++ b/playbooks/roles/tor-bridge/templates/tor-vanguards-update-and-verify.sh.j2 @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -eux + +# Cronjob/utility script to fetch updates to the "vanguards" Tor add-on +# https://github.com/mikeperry-tor/vanguards + +# This script uses the latest git tag of the repo and Mike Perry's PGP key ID +# grabbed from https://2019.www.torproject.org/docs/signing-keys.html.en +# +# N.B. this script should be idempotent and will exit early if git reports no changes + +VANGUARD_REPO_DIR="{{ tor_vanguards_addon_directory }}" +VANGUARD_REPO_URL="{{ tor_vanguards_repo_url }}" +VANGUARD_SIGNING_KEY="{{ tor_vanguards_addon_gpg_key }}" + +cd $VANGUARD_REPO_DIR +# Store currently checked out commit SHA in case anything fails. +# If we can't verify a new tag, we can checkout the old one which we know has been +# verified by Ansible during Streisand install. +VANGUARD_CURRENT_COMMIT=$(git show --format="%H" --no-patch) +VANGUARD_CURRENT_TAG=$(git describe --tags) + +# Failsafe function to ensure we're checked out into the last "known" good git tag/commit +# This way, if a git pull/gpg verify/other command in this script fails, we can leave +# the vanguards git repo in a known "good state" and hopefully not break anything. +function git_checkout_known_tag() { + git checkout $VANGUARD_CURRENT_COMMIT +} +trap git_checkout_known_tag ERR SIGINT SIGTERM SIGQUIT SIGKILL + +function check_for_git_updates() { + # Since individual commits aren't verified in the repo (only tags), + # we compare the number of tags in the local repo vs the remote repo. + # git ls-remote in this case communicates on the fly with the remote repo + # for an up-to-date list of tags in the project. If we don't have any new + # tags to fetch, we can exit early. We can even avoid doing "git fetch" this way. + local local_tag_count=$(git tag -l | wc -l) + # The grep -v is to strip away "tag references" which will show up for each annotated tag. + # otherwise we get a bigger list of tags (e.g. "refs/tags/v0.0.1", "refs/tags/v0.0.1^{}" etc.) + local remote_tag_count=$(git ls-remote --tags origin | grep -v "\^{}" | wc -l) + if [ $local_tag_count == $remote_tag_count ]; then + echo "Local/remote tag count is the same, nothing to do." + exit 0 + fi + # Repo has new tags, so we should do a fetch + git fetch +} + +function verify_and_update() { + # Find latest available tag by sorting by "taggerdate" (date tag was created) + # We also want to avoid ever checking out an alpha/beta/dev tag and only use "stable" tags + LATEST_TAG=$(git tag -l --sort=-taggerdate | grep -v "alpha\|beta\|dev\|test" | head -n 1) + # If it's the same tag we're currently using we don't need to do anything + # (means we likely only fetched beta/dev tags) + if [ "$LATEST_TAG" == "$VANGUARD_CURRENT_TAG" ]; then + echo "Already on the latest stable tag. Nothing to do." + exit 0 + fi + + # So now, we can finally verify! + # This should have been run during install, but just in case, we always configure + # git to use gpg2 instead of default /usr/bin/gpg + git config --global gpg.program $(which gpg2) + # Tell GPG which keyring to use (Streisand installs to a non-default location) + export GNUPGHOME="{{ streisand_gpg_dir }}" + # Make sure the verify-tag command is checking against the right signing key + # --raw redirects output to stderr and gives us full raw output from gpg + # if sig is valid we should find "VALIDSIG [...] KEY_ID_HERE" in the output + git verify-tag --raw "$LATEST_TAG" 2>&1 | grep -q "VALIDSIG.*${VANGUARD_SIGNING_KEY#0x}" + + # If we get this far, we can confirm the latest tag is signed and should be OK to use. + # (git verify-tag will exit with an error status if it doesn't succeed) + # Now we can do a checkout of that tag inside the git repo. + git checkout $LATEST_TAG + + # Restart the systemd service to run the updated code + systemctl restart vanguards.service +} + +check_for_git_updates +verify_and_update +exit $? diff --git a/playbooks/roles/tor-bridge/templates/torrc-hidden-service.j2 b/playbooks/roles/tor-bridge/templates/torrc-hidden-service.j2 new file mode 100644 index 000000000..768b96fdb --- /dev/null +++ b/playbooks/roles/tor-bridge/templates/torrc-hidden-service.j2 @@ -0,0 +1,19 @@ +# Use the local bridge to connect to the Tor network as a hidden service. +# We also use a separate tor process to host the hidden service, instead of +# hosting both the Streisand bridge and Streisand gateway hidden service +# on the same tor process. Otherwise, we risk leaking a lot of information +# about our hidden service possibly leading to deanonymization. +# see https://trac.torproject.org/projects/tor/ticket/8742 +UseBridges 1 +Bridge 127.0.0.1:{{ tor_orport }} +SocksPort 0 +User {{ tor_hidden_service_user }} + +HiddenServiceDir {{ tor_hidden_service_directory }} +HiddenServicePort 80 {{ tor_internal_hidden_service_address }} + +DataDirectory {{ tor_hidden_service_state_directory }} +PidFile {{ tor_hidden_service_run_directory }}/tor.pid + +ControlSocket {{ tor_hidden_service_run_directory }}/control GroupWritable RelaxDirModeCheck +CookieAuthFile {{ tor_hidden_service_run_directory }}/control.authcookie diff --git a/playbooks/roles/tor-bridge/templates/torrc.j2 b/playbooks/roles/tor-bridge/templates/torrc.j2 index 98d5e5689..f1e4fb4ff 100644 --- a/playbooks/roles/tor-bridge/templates/torrc.j2 +++ b/playbooks/roles/tor-bridge/templates/torrc.j2 @@ -9,6 +9,3 @@ Nickname {{ tor_bridge_nickname.stdout }} ServerTransportPlugin obfs4 exec /usr/bin/obfs4proxy ServerTransportListenAddr obfs4 0.0.0.0:{{ tor_obfs4_port }} - -HiddenServiceDir {{ tor_hidden_service_directory }} -HiddenServicePort 80 {{ tor_internal_hidden_service_address }} diff --git a/playbooks/roles/tor-bridge/templates/vanguards.conf.j2 b/playbooks/roles/tor-bridge/templates/vanguards.conf.j2 new file mode 100644 index 000000000..da06a05de --- /dev/null +++ b/playbooks/roles/tor-bridge/templates/vanguards.conf.j2 @@ -0,0 +1,4 @@ +## Global options, override default control socket/state file +[Global] +control_socket = {{ tor_hidden_service_run_directory }}/control +state_file = {{ tor_hidden_service_state_directory }}/vanguards.state diff --git a/playbooks/roles/tor-bridge/templates/vanguards.service.j2 b/playbooks/roles/tor-bridge/templates/vanguards.service.j2 new file mode 100644 index 000000000..cfceb8011 --- /dev/null +++ b/playbooks/roles/tor-bridge/templates/vanguards.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Additional protections for Tor onion services +After=network.target tor-hidden-service.service + +[Service] +User={{ tor_hidden_service_user }} +Group={{ tor_hidden_service_user }} +# Vanguards will check this env variable on startup and use the value as its config file +Environment=VANGUARDS_CONFIG={{ tor_vanguards_config }} +# Use virtualenv binary installed by vanguards setup script +ExecStart={{ tor_vanguards_addon_directory }}/vanguardenv/bin/vanguards +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/playbooks/roles/tor-bridge/vars/main.yml b/playbooks/roles/tor-bridge/vars/main.yml index 9ea1e3dbf..1c01234cf 100644 --- a/playbooks/roles/tor-bridge/vars/main.yml +++ b/playbooks/roles/tor-bridge/vars/main.yml @@ -6,8 +6,13 @@ tor_standard_connection_details: "{{ streisand_ipv4_address }}:{{ tor_orport }}" tor_obfs4_bridge_line: "obfs4 {{ streisand_ipv4_address }}:{{ tor_obfs4_port }} {{ tor_fingerprint.stdout }} cert={{ tor_obfs4_certificate.stdout }} iat-mode=0" tor_state_directory: "/var/lib/tor" - -tor_hidden_service_directory: "{{ tor_state_directory }}/hidden_service/" +# Hidden service is run as a separate tor process to avoid leaking data/deanonymization +tor_hidden_service_state_directory: "/var/lib/tor-hidden-service" +tor_hidden_service_run_directory: "/run/tor-hidden-service" +tor_hidden_service_directory: "{{ tor_hidden_service_state_directory }}/hidden_service/" +tor_hidden_service_log_directory: "/var/log/tor-hidden-service" +tor_hidden_service_config: "/etc/tor/torrc-hidden-service" +tor_hidden_service_user: "debian-tor-hs" tor_obfs_state_directory: "{{ tor_state_directory }}/pt_state" @@ -19,3 +24,10 @@ tor_html_instructions: "{{ tor_gateway_location }}/index.html" tor_obfs4_qr_code: "{{ tor_gateway_location }}/tor-obfs4-qr-code.png" tor_internal_hidden_service_address: "127.0.0.1:8181" +# Vanguards add-on git tags are signed by Tor developer Mike Perry +# see https://2019.www.torproject.org/docs/signing-keys.html.en +# for source of key ID. +tor_vanguards_addon_gpg_key: "0xC963C21D63564E2B10BB335B29846B3C683686CC" +tor_vanguards_addon_directory: "/usr/lib/tor/vanguards" +tor_vanguards_repo_url: "https://github.com/mikeperry-tor/vanguards.git" +tor_vanguards_config: "/etc/tor/vanguards.conf"