- name: create directory for ssh hidden service file: path: /var/lib/tor/ssh state: directory owner: debian-tor group: debian-tor mode: '0700' - name: ensure ssh hidden service is configured in torrc blockinfile: path: /etc/tor/torrc marker: "# {mark} ANSIBLE MANAGED SSH HIDDEN SERVICE" block: | HiddenServiceDir /var/lib/tor/ssh HiddenServicePort 22 127.0.0.1:22 notify: restart tor - name: wait for hidden service hostname file wait_for: path: /var/lib/tor/ssh/hostname timeout: 120 - name: wait for hidden service secret key file wait_for: path: /var/lib/tor/ssh/hs_ed25519_secret_key timeout: 120 - name: read hidden service hostname slurp: src: /var/lib/tor/ssh/hostname register: hs_hostname_raw - name: set fact with cleaned .onion hostname set_fact: onion_address: "{{ hs_hostname_raw.content | b64decode | trim }}" - name: ensure ssh key for onion access exists on target openssh_keypair: path: /root/.ssh/id_ed25519_onion type: ed25519 owner: root group: root mode: '0600' register: onion_ssh_key - name: read onion ssh public key from target slurp: src: /root/.ssh/id_ed25519_onion.pub register: onion_ssh_pubkey_raw - name: add onion ssh public key to authorized_keys authorized_key: user: root state: present key: "{{ onion_ssh_pubkey_raw.content | b64decode }}" - name: create local directory for ssh config.d file: path: "./{{ onion_address }}/config.d" state: directory mode: '0700' become: false delegate_to: localhost run_once: true - name: generate modular SSH config file copy: dest: "./{{ onion_address }}/ssh_config" content: | Include config.d/* Host *.onion ProxyCommand nc -x 127.0.0.1:9050 -X 5 %h %p become: false delegate_to: localhost run_once: true - name: fetch generated onion ssh private key fetch: src: /root/.ssh/id_ed25519_onion dest: "./{{ onion_address }}/{{ onion_address }}_id_ed25519" flat: yes - name: set correct permissions on fetched hidden service private key file: path: "./{{ onion_address }}/{{ onion_address }}_id_ed25519" mode: '0600' delegate_to: localhost - name: fetch generated onion ssh public key fetch: src: /root/.ssh/id_ed25519_onion.pub dest: "./{{ onion_address }}/{{ onion_address }}_id_ed25519.pub" flat: yes - name: fetch hidden service private key fetch: src: "/var/lib/tor/ssh/hs_ed25519_secret_key" dest: "./{{ onion_address }}/hs_ed25519_secret_key" flat: yes mode: '0600' - name: generate onion-specific ssh config snippet copy: dest: "./{{ onion_address }}/config.d/{{ onion_address }}" content: | Host {{ onion_address }} HostName {{ onion_address }} Port 22 User {{ ansible_user | default('root') }} PubkeyAuthentication yes IdentityFile ../{{ onion_address }}_id_ed25519 VerifyHostKeyDNS no become: false delegate_to: localhost run_once: true - name: generate ssh wrapper script copy: dest: "./{{ onion_address }}/ssh_{{ onion_address }}" content: | #!/bin/bash SOCKS_PORT=9050 SOCKS_HOST=127.0.0.1 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="$SCRIPT_DIR/ssh_config" KEY_FILE="$SCRIPT_DIR/{{ onion_address }}_id_ed25519" ONION_HOST="{{ onion_address }}" USER="root" if ! pgrep -x tor >/dev/null; then printf "%s\n" "[err] tor process is not running" exit 1 fi if ! nc -z "$SOCKS_HOST" "$SOCKS_PORT"; then printf "%s\n" "[err] tor socks proxy $SOCKS_HOST:$SOCKS_PORT is not reachable" exit 1 fi if ! test -f "$CONFIG_FILE"; then printf "%s\n" "[err] ssh config not found: $CONFIG_FILE" exit 1 fi if ! test -f "$KEY_FILE"; then printf "%s\n" "[err] identity file not found: $KEY_FILE" exit 1 fi exec ssh \ -F "$CONFIG_FILE" \ -i "$KEY_FILE" \ "$USER@$ONION_HOST" \ "$@" mode: '0755' become: false delegate_to: localhost run_once: true