diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py index 2300701222..cb94b9eb84 100644 --- a/admin/tests/test_securedrop-admin.py +++ b/admin/tests/test_securedrop-admin.py @@ -665,7 +665,7 @@ def test_only_v3_onion_services(self, tmpdir): ansible_path='.', app_path=dirname(__file__)) site_config = securedrop_admin.SiteConfig(args) - with open("app-source-ths", "w") as fobj: + with open("app-sourcev3-ths", "w") as fobj: fobj.write("a" * 56 + ".onion\n") site_config.update_onion_version_config() site_config.save() @@ -676,7 +676,7 @@ def test_only_v3_onion_services(self, tmpdir): v2_onion_services: false v3_onion_services: true """) - os.remove("app-source-ths") + os.remove("app-sourcev3-ths") assert expected == data def test_validate_gpg_key(self, caplog): diff --git a/install_files/ansible-base/inventory-dynamic b/install_files/ansible-base/inventory-dynamic index 0f2adbefe0..6520be98d7 100755 --- a/install_files/ansible-base/inventory-dynamic +++ b/install_files/ansible-base/inventory-dynamic @@ -92,9 +92,9 @@ def lookup_admin_username(): return admin_username -def lookup_tor_hostname(hostname): +def lookup_tor_v2_hostname(hostname): """ - Extract Onion URL from HidServAuth file that was fetched back locally. + Extract Onion v2 URL from HidServAuth file that was fetched back locally. Returns Onion URL for given inventory hostname. """ aths_path = os.path.join(SECUREDROP_ANSIBLE_DIRECTORY, @@ -106,13 +106,32 @@ def lookup_tor_hostname(hostname): # assuming the file is a raw `hostname` file generated by tor, # but the SD playbooks format the line with `HidServAuth` prefix, # so it can be concatenated into the torrc file on Tails. - tor_hostname = tor_config[1] + tor_v2_hostname = tor_config[1] except IndexError: - msg = ("Tor config file for '{}' ", + msg = ("Tor v2 config file for '{}' ", "appears to be empty").format(hostname) raise Exception(msg=msg) - return tor_hostname + return tor_v2_hostname + + +def lookup_tor_v3_hostname(hostname): + """ + Extract Onion v3 URL from .auth_private file that was fetched back locally. + Returns Onion URL for given inventory hostname. + """ + aths_path = os.path.join(SECUREDROP_ANSIBLE_DIRECTORY, + "{}-ssh.auth_private".format(hostname)) + with io.open(aths_path, 'r') as f: + tor_config = f.readline().rstrip().split(":") + try: + tor_v3_hostname = "{}.onion".format(tor_config[0]) + except IndexError: + msg = ("Tor v3 config file for '{}' ", + "appears to be empty").format(hostname) + raise Exception(msg=msg) + + return tor_v3_hostname def lookup_ssh_address(hostname): @@ -122,10 +141,13 @@ def lookup_ssh_address(hostname): """ ssh_address = lookup_local_ipv4_address(hostname) try: - ssh_address = lookup_tor_hostname(hostname) + ssh_address = lookup_tor_v3_hostname(hostname) # Don't assume ATHS files are present; they won't be on first run. except (IndexError, EnvironmentError): - pass + try: + ssh_address = lookup_tor_v2_hostname(hostname) + except (IndexError, EnvironmentError): + pass return ssh_address diff --git a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py index 53e2614c11..7bd6f19710 100644 --- a/install_files/ansible-base/roles/tails-config/files/securedrop_init.py +++ b/install_files/ansible-base/roles/tails-config/files/securedrop_init.py @@ -7,6 +7,8 @@ import sys import subprocess +from shutil import copyfile + # check for root if os.geteuid() != 0: @@ -26,6 +28,16 @@ path_gui_updater = os.path.join(path_securedrop_root, 'journalist_gui/SecureDropUpdater') +paths_v3_authfiles = { + "app-journalist": os.path.join(path_securedrop_root, + 'install_files/ansible-base/app-journalist.auth_private'), + "app-ssh": os.path.join(path_securedrop_root, + 'install_files/ansible-base/app-ssh.auth_private'), + "mon-ssh": os.path.join(path_securedrop_root, + 'install_files/ansible-base/mon-ssh.auth_private') +} +path_onion_auth_dir = '/var/lib/tor/onion_auth' + # load torrc_additions if os.path.isfile(path_torrc_additions): with io.open(path_torrc_additions) as f: @@ -52,11 +64,35 @@ with io.open(path_torrc, 'w') as f: f.write(torrc + torrc_additions) -# reload tor +# check for v3 aths files +v3_authfiles_present = False +for f in paths_v3_authfiles.values(): + if os.path.isfile(f): + v3_authfiles_present = True + +# if there are v3 authfiles, make dir and copy them into place +debian_tor_uid = pwd.getpwnam("debian-tor").pw_uid +debian_tor_gid = grp.getgrnam("debian-tor").gr_gid + +if not os.path.isdir(path_onion_auth_dir): + os.mkdir(path_onion_auth_dir) + +os.chmod(path_onion_auth_dir, 0o700) +os.chown(path_onion_auth_dir, debian_tor_uid, debian_tor_gid) + +for key, f in paths_v3_authfiles.items(): + if os.path.isfile(f): + filename = os.path.basename(f) + new_f = os.path.join(path_onion_auth_dir, filename) + copyfile(f, new_f) + os.chmod(new_f, 0o400) + os.chown(new_f, debian_tor_uid, debian_tor_gid) + +# restart tor try: - subprocess.check_call(['systemctl', 'reload', 'tor@default.service']) + subprocess.check_call(['systemctl', 'restart', 'tor@default.service']) except subprocess.CalledProcessError: - sys.exit('Error reloading Tor') + sys.exit('Error restarting Tor') # Turn off "automatic-decompression" in Nautilus to ensure the original # submission filename is restored (see diff --git a/install_files/ansible-base/roles/tails-config/tasks/cleanup_legacy_artifacts.yml b/install_files/ansible-base/roles/tails-config/tasks/cleanup_legacy_artifacts.yml deleted file mode 100644 index 127c07f050..0000000000 --- a/install_files/ansible-base/roles/tails-config/tasks/cleanup_legacy_artifacts.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -# Legacy changes handler to ensure backwards-compatibility. If an Admin runs -# this config after already having set up SecureDrop before, any outdated -# items will be handled and updated accordingly. - -- name: Remove deprecated network hook config files. - become: yes - file: - path: "{{ item.0 }}/{{ item.1 }}" - state: absent - with_nested: - - "{{ tails_config_deprecated_directories }}" - - "{{ tails_config_deprecated_config_files }}" - -- name: Remove deprecated xsessionrc file. - become: yes - file: - path: "{{ tails_config_live_persistence }}/dotfiles/.xsessionrc" - state: absent - -- name: Remove deprecated Document Interface desktop icons. - become: yes - file: - state: absent - path: "{{ item }}" - with_items: - - "{{ tails_config_amnesia_home }}/Desktop/document.desktop" - - "{{ tails_config_amnesia_home }}/.local/share/applications/document.desktop" - - "{{ tails_config_amnesia_home }}/.securedrop/document.desktop" - - "{{ tails_config_live_persistence }}/Desktop/document.desktop" - - "{{ tails_config_live_persistence }}/dotfiles/Desktop/document.desktop" - - "{{ tails_config_live_persistence }}/dotfiles/.local/share/applications/document.desktop" - - "{{ tails_config_live_persistence }}/Persistent/.securedrop/document.desktop" diff --git a/install_files/ansible-base/roles/tails-config/tasks/configure_torrc_additions.yml b/install_files/ansible-base/roles/tails-config/tasks/configure_torrc_additions.yml index ec84c81bf6..69d9d3ce88 100644 --- a/install_files/ansible-base/roles/tails-config/tasks/configure_torrc_additions.yml +++ b/install_files/ansible-base/roles/tails-config/tasks/configure_torrc_additions.yml @@ -9,17 +9,29 @@ - '*-aths' register: find_aths_info_result -# We need at least one ATHS value, for the Journalist Interface. -# Admin Workstations will have three, including the two SSH interfaces. +- name: Find V3 Authenticated Onion Service info for SecureDrop interfaces. + find: + paths: + - "{{ tails_config_ansible_base }}" + patterns: + # Collect all files that end in `.auth_private` - if there are any present + # then `torrc` will need a directive added + - '*.auth_private' + register: find_v3_aths_info_result + +# We need at least one v2 or v3 ATHS value, for the Journalist Interface. +# If v2 is enabled, there will be 3 v2 `-aths` files on the Admin Interface. +# If v3 is enabled, there will be 3 v3 `.auth_private` files on the Admin Interface. +# If both are enabled, the Admin Interface will have 6 files in total. # This task simply validates that at least one suitable file was found; # if not, then the playbooks haven't been run, so fail with instructions. - name: Confirm ATHS info was found. assert: that: - - find_aths_info_result.matched >= 1 + - find_aths_info_result.matched + find_v3_aths_info_result.matched >= 1 msg: >- Failed to find ATHS info locally. Make sure you've installed SecureDrop - on the servers, and that the `-aths` files are located in: + on the servers, and that the `-aths` and/or `.auth_private` files are located in: `{{ tails_config_ansible_base }}/`. - name: Assemble ATHS info into torrc additions. @@ -31,3 +43,13 @@ owner: root group: root mode: "0400" + +- name: Append ClientOnionAuthDir directive to torrc additions + become: yes + lineinfile: + dest: "{{ tails_config_torrc_additions }}" + line: "ClientOnionAuthDir /var/lib/tor/onion_auth" + owner: root + group: root + mode: "0400" + when: find_v3_aths_info_result.matched >= 1 diff --git a/install_files/ansible-base/roles/tails-config/tasks/create_desktop_shortcuts.yml b/install_files/ansible-base/roles/tails-config/tasks/create_desktop_shortcuts.yml index 22cc4eda63..3b229ed201 100644 --- a/install_files/ansible-base/roles/tails-config/tasks/create_desktop_shortcuts.yml +++ b/install_files/ansible-base/roles/tails-config/tasks/create_desktop_shortcuts.yml @@ -3,15 +3,43 @@ # installation. On the Admin Workstation, these files will be present # after running the playbooks, but on the Journalist Workstation, they must # be copied manually by the Admin. -- name: Look up Source Interface URL. +# Desktop shortcuts default to v3 URLs when available - as `site-specific` +# is not copied to Journalist Workstations, the {v2,v3}_onion_service +# booleans can't be used to choose a preference. + +- name: Check for v3 Source Interface file + stat: + path: app-sourcev3-ths + register: v3_source_file + +- name: Check for v3 Journalist Interface file + stat: + path: app-journalist.auth_private + register: v3_journalist_file + +- name: Look up v2 Source Interface URL. command: grep -Po '.{16}\.onion' app-source-ths changed_when: false register: source_interface_lookup_result + when: v3_source_file.stat.exists == false -- name: Look up Journalist Interface URL. +- name: Look up v3 Source Interface URL. + command: grep -Po '.{56}\.onion' app-sourcev3-ths + changed_when: false + register: sourcev3_interface_lookup_result + when: v3_source_file.stat.exists == true + +- name: Look up v2 Journalist Interface URL. command: grep -Po '.{16}\.onion' app-journalist-aths changed_when: false register: journalist_interface_lookup_result + when: v3_source_file.stat.exists == false + +- name: Look up v3 Journalist Interface URL. + command: awk -F ':' '{print $1 ".onion"}' app-journalist.auth_private + changed_when: false + register: journalistv3_interface_lookup_result + when: v3_source_file.stat.exists == true - name: Create desktop shortcut parent directories. file: @@ -30,16 +58,27 @@ when: item.startswith(tails_config_amnesia_home) with_items: "{{ tails_config_desktop_icon_directories }}" +- name: Set the right variable for source + set_fact: + source_iface: "{{ sourcev3_interface_lookup_result if (v3_source_file.stat.exists == true) else source_interface_lookup_result }}" + +- name: Set the right variable for journalist + set_fact: + journalist_iface: "{{ journalistv3_interface_lookup_result if (v3_source_file.stat.exists == true) else journalist_interface_lookup_result }}" + +- debug: + var: source_iface + # Storing as host fact so we can loop over the data in one task. - name: Assemble desktop icon info. set_fact: _securedrop_desktop_icon_info: - src: desktop-source-icon.j2 filename: source.desktop - onion_url: "{{ source_interface_lookup_result.stdout }}" + onion_url: "{{ source_iface.stdout }}" - src: desktop-journalist-icon.j2 filename: journalist.desktop - onion_url: "{{ journalist_interface_lookup_result.stdout }}" + onion_url: "{{ journalist_iface.stdout }}" - name: Create SecureDrop interface desktop icons. become: yes diff --git a/install_files/ansible-base/roles/tails-config/tasks/create_ssh_aliases.yml b/install_files/ansible-base/roles/tails-config/tasks/create_ssh_aliases.yml index 129bf3a651..c47aecce3d 100644 --- a/install_files/ansible-base/roles/tails-config/tasks/create_ssh_aliases.yml +++ b/install_files/ansible-base/roles/tails-config/tasks/create_ssh_aliases.yml @@ -2,7 +2,7 @@ - name: Import variables include_vars: "group_vars/all/site-specific" -- name: Lookup onion ssh files +- name: Lookup v2 onion ssh files stat: path: "{{ item }}-ssh-aths" register: "ssh_onion_lookup" @@ -10,6 +10,14 @@ - app - mon +- name: Lookup v3 onion ssh files + stat: + path: "{{ item }}-ssh.auth_private" + register: "ssh_v3_onion_lookup" + with_items: + - app + - mon + - name: Hacky work-around to get below logic working set_fact: mon_ip: "{{ monitor_ip }}" @@ -18,6 +26,13 @@ assert: that: "item.stat.exists or {{item.item}}_ip is defined" with_items: "{{ ssh_onion_lookup.results }}" + when: v2_onion_services == True + +- name: Confirm that either the app v3 onion ssh file exists or site-specific file exists + assert: + that: "item.stat.exists or {{item.item}}_ip is defined" + with_items: "{{ ssh_v3_onion_lookup.results }}" + when: v3_onion_services == True - name: Create SSH config directory. become: yes diff --git a/install_files/ansible-base/roles/tails-config/tasks/main.yml b/install_files/ansible-base/roles/tails-config/tasks/main.yml index 4477d7c6ff..c4508a68ae 100644 --- a/install_files/ansible-base/roles/tails-config/tasks/main.yml +++ b/install_files/ansible-base/roles/tails-config/tasks/main.yml @@ -2,10 +2,6 @@ # Reuse validation logic. - include: "{{ role_path }}/../validate/tasks/validate_tails_environment.yml" -- include: cleanup_legacy_artifacts.yml - -- include: migrate_existing_config.yml - - include: copy_dotfiles.yml - include: configure_torrc_additions.yml diff --git a/install_files/ansible-base/roles/tails-config/tasks/migrate_existing_config.yml b/install_files/ansible-base/roles/tails-config/tasks/migrate_existing_config.yml deleted file mode 100644 index a85bc710d6..0000000000 --- a/install_files/ansible-base/roles/tails-config/tasks/migrate_existing_config.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -# Between 0.3.12 and 0.4, the "Document Interface" was renamed to -# "Journalist Interface". Since multiple scripts expect to readfrom this file, -# we'll rename the old file to handle the transition gracefully. -- name: Migrate Document Interface ATHS file (upgrade only). - command: >- - mv --no-clobber - "{{ tails_config_ansible_base }}/app-document-aths" - "{{ tails_config_ansible_base }}/app-journalist-aths" - args: - # Only run this command if the deprecated file is present. - removes: "{{ tails_config_ansible_base }}/app-document-aths" - -# Between 0.3.12 and 0.4, the `prod-specific.yml` vars file was deprecated -# in favor of `group_vars/all/site-specific`. The latter is not version -# controlled, and so will not exist by default. -- name: Migrate site-specific configuration to new location. - command: >- - cp - "{{ tails_config_ansible_base }}/prod-specific.yml" - "{{ tails_config_ansible_base }}/group_vars/all/site-specific" - register: site_specific_vars_migration_result - args: - # Only run this command if no site-specific vars exist. First-time - # installs will create the vars via `./securedrop-admin sdconfig`. - creates: "{{ tails_config_ansible_base }}/group_vars/all/site-specific" - -# If the site-specific vars file was migrated in the previous task, we must -# explicitly include it, since it wasn't present in group_vars/all/ at the -# start of the play, so none of its variables will be defined. -- name: Import newly migrated sites-specific configuration. - include_vars: "{{ tails_config_ansible_base }}/group_vars/all/site-specific" - when: site_specific_vars_migration_result|changed diff --git a/install_files/ansible-base/roles/tails-config/templates/ssh_config.j2 b/install_files/ansible-base/roles/tails-config/templates/ssh_config.j2 index 1b1eeacd0f..6bba3212db 100644 --- a/install_files/ansible-base/roles/tails-config/templates/ssh_config.j2 +++ b/install_files/ansible-base/roles/tails-config/templates/ssh_config.j2 @@ -1,4 +1,7 @@ {% set svc_grep = "grep -Po '.{16}\.onion' svc-ssh-aths" %} +{% set svc_awk = "awk -F ':' '{print $1 \".onion\"}' svc-ssh.auth_private" %} + +{% if v2_onion_services and not v3_onion_services -%} {% for svc in ssh_onion_lookup.results %} Host {{ svc.item }} {% set svc_grep = "grep -Po '.{16}\.onion' "+svc.item+"-ssh-aths" -%} @@ -12,3 +15,36 @@ Host {{ svc.item }} {% endif %} {% endfor %} +{% endif %} + +{% if v3_onion_services -%} +{% for svc in ssh_v3_onion_lookup.results %} +Host {{ svc.item }} + {% set svc_awk = "awk -F ':' '{print $1 \".onion\"}' "+svc.item+"-ssh.auth_private" -%} + {% set direct_ip = hostvars[inventory_hostname][svc.item+'_ip'] -%} + User {{ ssh_users }} + Hostname {{ lookup('pipe', svc_awk) if (svc.stat.exists and enable_ssh_over_tor) else direct_ip }} + {% if enable_ssh_over_tor and svc.stat.exists -%} + ProxyCommand /bin/nc -X 5 -x 127.0.0.1:9050 %h %p + {% else -%} + ProxyCommand none + {% endif %} + +{% endfor %} +{% endif %} + +{% if v2_onion_services and v3_onion_services -%} +{% for svc in ssh_onion_lookup.results %} +Host {{ svc.item + "-legacy" }} + {% set svc_grep = "grep -Po '.{16}\.onion' "+svc.item+"-ssh-aths" -%} + {% set direct_ip = hostvars[inventory_hostname][svc.item+'_ip'] -%} + User {{ ssh_users }} + Hostname {{ lookup('pipe', svc_grep) if (svc.stat.exists and enable_ssh_over_tor) else direct_ip }} + {% if enable_ssh_over_tor and svc.stat.exists -%} + ProxyCommand /bin/nc -X 5 -x 127.0.0.1:9050 %h %p + {% else -%} + ProxyCommand none + {% endif %} + +{% endfor %} +{% endif %} diff --git a/install_files/ansible-base/securedrop-tails.yml b/install_files/ansible-base/securedrop-tails.yml index e63439a2ae..818bcf4356 100755 --- a/install_files/ansible-base/securedrop-tails.yml +++ b/install_files/ansible-base/securedrop-tails.yml @@ -20,8 +20,8 @@ Successfully configured Tor and set up desktop bookmarks for SecureDrop! You will see a notification appear on your screen when Tor is ready. - The Journalist Interface's Tor onion URL is: http://{{ journalist_interface_lookup_result.stdout }} - The Source Interfaces's Tor onion URL is: http://{{ source_interface_lookup_result.stdout }} + The Journalist Interface's Tor onion URL is: http://{{ journalist_iface.stdout }} + The Source Interfaces's Tor onion URL is: http://{{ source_iface.stdout }} {% if find_aths_info_result.matched > 1 %} SSH aliases are set up. You can use them with 'ssh app' and 'ssh mon'. {% endif %}