From c03384df724abd80aca8d0596cb87b98ceaddac9 Mon Sep 17 00:00:00 2001 From: vaznasty Date: Tue, 13 Jan 2026 00:44:00 -0500 Subject: [PATCH] create onion recipe --- .drone.yml | 10 ++++---- .env.sample | 21 +++++++++++++---- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++---------- abra.sh | 5 ++++ compose.yml | 54 ++++++++++++++++++++++++------------------ torrc.tmpl | 16 +++++++++++++ 6 files changed, 129 insertions(+), 45 deletions(-) create mode 100644 torrc.tmpl diff --git a/.drone.yml b/.drone.yml index 8139990..7337d6d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -6,17 +6,17 @@ steps: image: git.coopcloud.tech/coop-cloud/stack-ssh-deploy:latest settings: host: swarm-test.autonomic.zone - stack: example_com # UPDATE ME - generate_secrets: true + stack: onion purge: true deploy_key: from_secret: drone_ssh_swarm_test networks: - proxy environment: - DOMAIN: example.swarm-test.autonomic.zone # UPDATE ME - STACK_NAME: example_com # UPDATE ME - LETS_ENCRYPT_ENV: staging + DOMAIN: onion.swarm-test.autonomic.zone + STACK_NAME: onion + TRAEFIK_SERVICE: traefik.swarm-test.autonomic.zone + TORRC_CONFIG_VERSION: v1 # Also set any config versions from abra.sh trigger: branch: diff --git a/.env.sample b/.env.sample index e5b13b4..f75d2c5 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,19 @@ -TYPE=onion-service +TYPE=onion -DOMAIN=onion-service.example.com +DOMAIN=onion.example.com -## Domain aliases -#EXTRA_DOMAINS=', `www.onion-service.example.com`' +# Disabled by default because this will backup the sensitive onion secret key +ENABLE_BACKUPS=false -LETS_ENCRYPT_ENV=production +# Include the full stack name of your traefik service plus "_app" +# For example, if your traefik is at domain traefik.example.com, set traefik_example_com_app +# TODO Can we automate setting this? +TRAEFIK_SERVICE=traefik_example_com_app +TRAEFIK_PORT=8080 # traefik port with the entrypoint to use +ONION_PORT=80 # port that will be exposed to clients of the onion service + +## Enabling these reduces latency but you will NOT be anonymous +## You must enable NON_ANONYMOUS_MODE in order to enable SINGLE_HOP_MODE +## If you enable this, you can NEVER run the same service anonymously +# NON_ANONYMOUS_MODE=1 +# SINGLE_HOP_MODE=1 diff --git a/README.md b/README.md index b88f62e..e9dd6c6 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,68 @@ # onion-service -> One line description of the recipe +Creates a [Tor .onion service](https://community.torproject.org/onion-services) for your Co-op Cloud applications * **Category**: Apps -* **Status**: 0 -* **Image**: [`onion-service`](https://hub.docker.com/r/onion-service), 4, upstream -* **Healthcheck**: No -* **Backups**: No -* **Email**: No +* **Status**: 3 +* **Image**: [`onimages/tor:alpine`](https://onionservices.torproject.org/apps/base/containers), 4, upstream +* **Healthcheck**: Yes +* **Backups**: Yes +* **Email**: N/A * **Tests**: No -* **SSO**: No +* **SSO**: N/A -## Quick start +## Basic Usage -* `abra app new onion-service --secrets` -* `abra app config ` -* `abra app deploy ` +1. Set up Docker Swarm and [`abra`] +2. Deploy [`coop-cloud/traefik`] +3. `abra app new onion` +4. `abra app config ` - You must at least set `$TRAEFIK_SERVICE` to the name of your traefik service. For example, if traefik's URL is `traefik.example.com`, this variable should be set to `traefik_example_com_app` +5. `abra app deploy ` +6. Retrieve the hostname of the created onion service: `abra app cmd app show_hostname` +7. If your app's recipe supports an onion service, run `abra app config ` on the app you would like the onion service to point to, and set `ONION_DOMAIN` to the output of the previous command. You can use sub-domains, like `chat.xxx.onion` -For more, see [`docs.coopcloud.tech`](https://docs.coopcloud.tech). +## Backup and restore + +Backups are disabled by default for this recipe because any meaningful backup will include the sensitive private key for your onion service. If this is stolen, an attacker could impersonate you to users of your service. To enable backups, set `ENABLE_BACKUPS=true`. + +Fortunately, this key will not change unless you generate a new .onion, so regular automated backups are likely unnecessary; it should be perfectly fine to let backup bot take one snapshot to store in a secure location and then disable backups again. + +Other useful data included in the backup includes the `authorized_clients` directory, so clients can still access your service after a restore. + +After restoring a backup, make sure to run `abra app restart app` so Tor can advertise the restored hostname. + +## Enabling client authorization + +Onion services allow you to use client authorization to prevent just anyone from accessing your service without your permission. To set this up, you need to first [create a public/private keypair](https://community.torproject.org/onion-services/advanced/client-auth/) and then copy it to the `authorized_clients` folder in your app's HiddenServiceDir (in this case, `/var/lib/tor/traefik/authorized_clients`. + +Assuming you followed the instructions from the linked tutorial to create your keypair, you can do this with `echo "descriptor:x25519:$(cat /tmp/k1.pub.key)" > pubkey.auth && abra app cp pubkey.auth /var/lib/tor/traefik/authorized_clients/1.auth`. You can replace `1.auth` with any descriptive name as long as it ends in `.auth`. You may see a couple `Operation not permitted` errors, but this is expected, and the key will have been successfully copied. + +After copying the file, run `abra app restart app` + +## Add support to another recipe + +On a networking level, adding support for an onion service to your recipe is fairly simple. Add a file called `compose.onion.yml` to your recipe with the following lines: + +``` +services: + app: + labels: + - "traefik.http.routers.${STACK_NAME}-onion.rule=Host(`${ONION_DOMAIN}`)" + - "traefik.http.routers.${STACK_NAME}-onion.entrypoints=onion" +``` + +And, in `.env.sample`, add the following: +``` +## Uncomment and set ONION_DOMAIN to enable onion service access +## Subdomains can also be used for ONION_DOMAIN, for example, chat.< ... >.onion +# COMPOSE_FILE="$COMPOSE_FILE:compose.onion.yml" +# ONION_DOMAIN=<56-char onion address>.onion +``` + +This creates a new traefik router for your recipe that recognizes the .onion domain requested by the client and routes requests to your application, bypassing forced SSL. + +On the application level, there may sometimes be some tinkering required to get an app to truly work with an onion service. Common issues relate to apps themselves forcing SSL, or expecting a specific domain name. diff --git a/abra.sh b/abra.sh index 13b5452..dcf39a2 100755 --- a/abra.sh +++ b/abra.sh @@ -1,2 +1,7 @@ # Set any config versions here # Docs: https://docs.coopcloud.tech/maintainers/handbook/#manage-configs +export TORRC_CONFIG_VERSION=v1 + +show_hostname() { + cat /var/lib/tor/traefik/hostname +} diff --git a/compose.yml b/compose.yml index 5e77224..7d9ab44 100644 --- a/compose.yml +++ b/compose.yml @@ -1,39 +1,47 @@ --- services: app: - image: nginx:1.27.5 + image: containers.torproject.org/tpo/onion-services/onimages/tor:alpine + volumes: + - "tor:/var/lib/tor" networks: - proxy deploy: restart_policy: condition: on-failure labels: - - "traefik.enable=true" - - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80" - - "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})" - - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - ## Edit the following line if you are using one, but not both, "Redirect" sections below - #- "traefik.http.routers.${STACK_NAME}.middlewares=${STACK_NAME}-redirectscheme,${STACK_NAME}-redirecthostname" - ## Redirect from EXTRA_DOMAINS to DOMAIN - # - "traefik.http.middlewares.${STACK_NAME}-redirecthostname.redirectregex.regex=^http[s]?://([^/]*)/(.*)" - # - "traefik.http.middlewares.${STACK_NAME}-redirecthostname.redirectregex.replacement=https://${DOMAIN}/$${2}" - # - "traefik.http.middlewares.${STACK_NAME}-redirecthostname.redirectregex.permanent=true" - ## Redirect HTTP to HTTPS - # - "traefik.http.middlewares.${STACK_NAME}-redirectscheme.redirectscheme.scheme=https" - # - "traefik.http.middlewares.${STACK_NAME}-redirectscheme.redirectscheme.permanent=true" - ## When you're ready for release, run "abra recipe sync " to set this - - "coop-cloud.${STACK_NAME}.version=" - ## Enable backups: https://docs.coopcloud.tech/maintainers/handbook/#how-do-i-configure-backuprestore - # - "backupbot.backup=true" - # - "backupbot.backup.path=/some/path" + - "traefik.enable=false" + - "coop-cloud.${STACK_NAME}.version=1.0.0+alpine" + - "backupbot.backup=${ENABLE_BACKUPS}" + - "backupbot.backup.volumes.tor.path=traefik" + configs: + - source: torrc_config + target: /etc/tor/torrc + mode: 0644 + command: + - "-f" + - "/etc/tor/torrc" + # Connects to Tor conrol port using cookie authentication and verifies bootstrapping completed healthcheck: - test: ["CMD", "curl", "-f", "http://localhost"] + test: test -n "$$( + printf '%s\n%s\n%s\n' + "AUTHENTICATE $$(xxd -p -c 32 /var/lib/tor/.tor/control_auth_cookie | tr -d "\n")" + "GETINFO status/bootstrap-phase" + "QUIT" | + nc 127.0.0.1 9051 | + grep "PROGRESS=100")" interval: 30s timeout: 10s - retries: 10 - start_period: 1m + +configs: + torrc_config: + name: ${STACK_NAME}_torrc_config_${TORRC_CONFIG_VERSION} + file: torrc.tmpl + template_driver: golang networks: proxy: external: true + +volumes: + tor: diff --git a/torrc.tmpl b/torrc.tmpl new file mode 100644 index 0000000..346b8b4 --- /dev/null +++ b/torrc.tmpl @@ -0,0 +1,16 @@ +RunAsDaemon 0 + +# If there's a use case to enable client functionality we can put this behind a config +SocksPort 0 +ControlPort 127.0.0.1:9051 +CookieAuthentication 1 + +HiddenServiceDir /var/lib/tor/traefik +HiddenServicePort {{ env "ONION_PORT" }} {{ env "TRAEFIK_SERVICE" }}:{{ env "TRAEFIK_PORT" }} + +{{ if eq (env "NON_ANONYMOUS_MODE") "1" -}} +HiddenServiceNonAnonymousMode 1 +{{ end -}} +{{ if eq (env "SINGLE_HOP_MODE") "1" -}} +HiddenServiceSingleHopMode 1 +{{ end -}}