Skip to content
This repository has been archived by the owner on Jun 4, 2021. It is now read-only.

Separate tor hidden service from bridge, harden HS with vanguards add-on #1639

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions playbooks/roles/tor-bridge/files/system_tor_hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# vim:syntax=apparmor
#include <tunables/global>

profile system_tor_hs flags=(attach_disconnected) {
#include <abstractions/tor>

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,
}
143 changes: 142 additions & 1 deletion playbooks/roles/tor-bridge/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions playbooks/roles/tor-bridge/templates/tor-hidden-service.service.j2
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 $?
19 changes: 19 additions & 0 deletions playbooks/roles/tor-bridge/templates/torrc-hidden-service.j2
Original file line number Diff line number Diff line change
@@ -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
3 changes: 0 additions & 3 deletions playbooks/roles/tor-bridge/templates/torrc.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
4 changes: 4 additions & 0 deletions playbooks/roles/tor-bridge/templates/vanguards.conf.j2
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions playbooks/roles/tor-bridge/templates/vanguards.service.j2
Original file line number Diff line number Diff line change
@@ -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
16 changes: 14 additions & 2 deletions playbooks/roles/tor-bridge/vars/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"