Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .github/workflows/pr-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,49 @@ jobs:
docker compose down -v
echo "✅ Compose backward compatibility verified."

- name: Verify Hardened Container Execution (Dropped Caps & No-New-Privileges)
run: |
echo "Testing container startup with dropped capabilities and no-new-privileges..."
HARDENED_ID=$(docker run -d \
--name torproxy-hardened-test \
--security-opt no-new-privileges:true \
--cap-drop ALL \
-p 8119:8118 \
-p 9051:9050 \
-p 8081:8080 \
torproxy:smoke-test)
sleep 5
./tests/integration/verify_container.sh "$HARDENED_ID" 8081 8119 9051
docker rm -f "$HARDENED_ID"
echo "✅ Hardened container startup and routing verified."

- name: Verify Backward Compatible Upgrade from Prior Release
run: |
echo "Testing upgrade from previous release with unchanged Compose configuration..."
# Test running previous published image
docker run -d --name torproxy-prev -p 8120:8118 -p 8082:8080 hapheus/torproxy:latest || true
sleep 5
if [ "$(docker ps -q -f name=torproxy-prev)" ]; then
curl -s http://127.0.0.1:8082/status || true
docker rm -f torproxy-prev
fi
# Test running candidate image on the exact same port mapping
docker run -d --name torproxy-upgrade -p 8120:8118 -p 8082:8080 torproxy:smoke-test
sleep 5
curl -s http://127.0.0.1:8082/status | grep -q "status"
docker rm -f torproxy-upgrade
echo "✅ Upgrade compatibility verified."

- name: Install and Run Trivy Vulnerability Scanner
run: |
echo "Installing Trivy security scanner..."
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
echo "Scanning container image torproxy:smoke-test for vulnerabilities..."
trivy image --severity CRITICAL,HIGH --ignore-unfixed --exit-code 0 torproxy:smoke-test

- name: Cleanup Containers
if: always()
run: |
docker rm -f torproxy-smoke || true
docker rm -f torproxy-smoke torproxy-hardened-test torproxy-prev torproxy-upgrade || true
docker compose down -v || true

13 changes: 13 additions & 0 deletions .github/workflows/publish-dockerhub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,18 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: mode=max
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Update Docker Hub Description
uses: peter-evans/dockerhub-description@v4
if: github.ref == 'refs/heads/main'
with:
username: hapheus
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: hapheus/torproxy
readme-filepath: ./DOCKERHUB.md
short-description: "Lightweight, secure Docker HTTP/SOCKS5 Tor proxy with local REST control API"

136 changes: 136 additions & 0 deletions DOCKERHUB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# torproxy

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/hapheus/torproxy)](https://hub.docker.com/r/hapheus/torproxy)
[![Architectures](https://img.shields.io/badge/arch-amd64%20%7C%20arm64%20%7C%20arm%2Fv7-blue)](https://hub.docker.com/r/hapheus/torproxy)

A lightweight, secure, and production-ready Docker container combining **Tor**, **Privoxy** (HTTP/HTTPS proxy), **SOCKS5**, and a local **REST control API**.

Source repository: [https://github.com/hapheus/torproxy](https://github.com/hapheus/torproxy)

---

## Key Features

- **HTTP/HTTPS Proxy (Port 8118)**: Powered by Privoxy, automatically resolving DNS lookups through Tor exit relays to prevent DNS leaks.
- **SOCKS5 Proxy (Port 9050)**: Standard Tor SOCKS5 interface supporting remote hostname resolution (`socks5h://`).
- **REST Control API (Port 8080)**: Inspect circuit status, trigger circuit rotation (`/reconnect`), query health diagnostics, and pull Prometheus metrics.
- **Fail-Closed Security**: Privoxy strictly routes to Tor SOCKS5; if Tor circuits drop, proxy requests fail immediately (500/503) rather than leaking to clearnet.
- **Authenticated Tor Control**: Local Tor ControlPort is secured using ephemeral 32-byte filesystem cookie authentication.
- **Non-Root Execution**: Runs under a dedicated, unprivileged system user (`torproxy`), never as root.
- **Multi-Architecture**: Built natively for `linux/amd64`, `linux/arm64`, and `linux/arm/v7`.
- **Zero Breaking Changes**: Guaranteed backward compatibility across minor releases.

---

## Quick Start

### Option 1: Docker Run

```bash
docker run -d \
--name torproxy \
--restart unless-stopped \
-p 127.0.0.1:8118:8118 \
-p 127.0.0.1:9050:9050 \
-p 127.0.0.1:8080:8080 \
hapheus/torproxy:latest
```

### Option 2: Docker Compose

```yaml
services:
torproxy:
image: hapheus/torproxy:latest
container_name: torproxy
restart: unless-stopped
ports:
- "127.0.0.1:8118:8118"
- "127.0.0.1:9050:9050"
- "127.0.0.1:8080:8080"
```

---

## Verifying Tor Connectivity

Wait a few seconds for Tor to bootstrap, then test:

```bash
# 1. Check REST API status
curl -s http://127.0.0.1:8080/status | jq .

# 2. Test HTTP proxy via Privoxy
curl --proxy http://127.0.0.1:8118 https://check.torproject.org/api/ip

# 3. Test SOCKS5 proxy (using remote DNS to prevent leaks)
curl --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip

# 4. Request a new Tor identity / circuit
curl -X POST http://127.0.0.1:8080/reconnect
```

---

## REST API Reference

The management API listens on port `8080` (HTTP):

| Endpoint | Methods | Description |
| -------- | ------- | ----------- |
| `/status` | `GET` | Tor bootstrap and current verified egress IP address. |
| `/ip` | `GET` | Current verified Tor egress IP address. |
| `/health` | `GET` | Multi-subsystem health check (`tor`, `proxies`, `egress`). Returns HTTP 200 when healthy, 503 during bootstrap/errors. |
| `/diagnostics` | `GET` | Uptime, reconnect counters, bootstrap details, and cache status. |
| `/metrics` | `GET` | Prometheus exposition format metrics. |
| `/reconnect` | `POST` (or `GET`) | Triggers `SIGNAL NEWNYM` to establish a new Tor circuit. |
| `/disconnect` | `POST` (or `GET`) | Disables Tor network traffic (`SETCONF DisableNetwork=1`). |
| `/connect` | `POST` (or `GET`) | Re-enables Tor network traffic (`SETCONF DisableNetwork=0`). |

> [!WARNING]
> The management API is unauthenticated by default. Bind it strictly to `127.0.0.1` or place it behind an authenticating reverse proxy if exposed across networks.

---

## Configuration & Environment Variables

| Variable | Default | Description |
| -------- | ------- | ----------- |
| `TORPROXY_LISTEN_ADDRESS` | `0.0.0.0` | Bind address for Privoxy and Tor inside the container. |
| `TORPROXY_LISTEN_PORT` | `8118` | Privoxy HTTP proxy port. |
| `TORPROXY_SOCKS_PORT` | `9050` | Tor SOCKS5 proxy port. |
| `TORPROXY_API_PORT` | `8080` | REST API port. |

---

## Production Hardening

For maximum security in production, run with a read-only root filesystem and drop all Linux capabilities:

```yaml
services:
torproxy:
image: hapheus/torproxy:latest
restart: unless-stopped
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:rw,noexec,nosuid,size=32M
- /run:rw,noexec,nosuid,size=16M
- /var/lib/tor:rw,noexec,nosuid,size=64M
- /var/log/privoxy:rw,noexec,nosuid,size=16M
ports:
- "127.0.0.1:8118:8118"
- "127.0.0.1:9050:9050"
- "127.0.0.1:8080:8080"
```

---

## License

This project is licensed under the [MIT License](https://github.com/hapheus/torproxy/blob/main/LICENSE).
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The lightweight API server on port `8080` manages circuits and monitors health w
| `/status`, `/` | `GET`, `POST` | `200` | Connection status and current exit IP. | `{"status":"connected","ip":"198.51.100.42"}` |
| `/ip` | `GET` | `200`, `503` | Current verified Tor exit IP. Returns 503 while bootstrapping. | `{"ip":"198.51.100.42"}` |
| `/health`, `/v1/health` | `GET` | `200`, `503` | Structured diagnostic health state across Tor, proxies, and egress. | `{"status":"healthy","tor":{"bootstrapped":true,"progress":100},"proxies":{"socks5":true},"egress":{"verified":true,"ip":"..."}}` |
| `/diagnostics`, `/v1/diagnostics` | `GET` | `200` | Operational telemetry (uptime, reconnect counts, bootstrap progress). | `{"uptime_seconds":120,"reconnect_total":2,"tor":{"bootstrapped":true}}` |
| `/metrics`, `/v1/metrics` | `GET` | `200` | Prometheus exposition format metrics for scrapers. | Plaintext Prometheus metrics. |
| `/reconnect` | `POST` (or `GET`) | `200` | Request a new Tor circuit (`SIGNAL NEWNYM`). Clears IP cache. | `{"action":"reconnect","status":"success"}` |
| `/disconnect` | `POST` (or `GET`) | `200` | Pause Tor network traffic (`DisableNetwork=1`). Fails closed. | `{"action":"disconnect","status":"success"}` |
| `/connect` | `POST` (or `GET`) | `200` | Resume Tor network traffic (`DisableNetwork=0`). | `{"action":"connect","status":"success"}` |
Expand Down Expand Up @@ -120,6 +122,7 @@ Ready-to-run use cases with hardened configurations:

| Example | Purpose | Guide |
| ------- | ------- | ----- |
| **Hardened Production** | Read-only rootfs, dropped capabilities, and memory-backed tmpfs. | [View Example](examples/hardened-production/) |
| **Browserless Chromium** | Headless browser automation and scraping routed through Tor. | [View Example](examples/browserless-chromium-via-tor/) |
| **CRW Web Scraper** | High-performance webpage-to-Markdown scraper via Tor proxy. | [View Example](examples/crw-web-scraper-via-tor/) |
| **SearXNG Metasearch** | Private, self-hosted search engine routing upstream queries via Tor. | [View Example](examples/searxng-via-tor/) |
Expand All @@ -128,11 +131,12 @@ Ready-to-run use cases with hardened configurations:

## Security Model and Trust Boundaries

- **Non-Root Execution**: Container processes run strictly under UID `999` (`toruser`).
- **Non-Root Execution**: Container processes run strictly under the unprivileged system user `torproxy`.
- **Authenticated ControlPort**: Tor's control port (`127.0.0.1:9051`) requires ephemeral filesystem cookie authentication (`CookieAuthentication 1`).
- **Fail-Closed Guarantee**: If Tor disconnects or crashes, Privoxy drops traffic and returns HTTP 503 instead of leaking to clearnet.
- **DNS Leak Prevention**: To prevent local DNS leaks, always use remote DNS resolution with SOCKS5 (`curl --socks5-hostname 127.0.0.1:9050` or `socks5h://`).
- **Threat Model**: See [SECURITY.md](SECURITY.md) for vulnerability reporting procedures, threat boundaries, and anonymity limitations.
- **Hardening Guide**: See [docs/hardening.md](docs/hardening.md) for read-only rootfs and dropped Linux capabilities.

---

Expand All @@ -141,6 +145,8 @@ Ready-to-run use cases with hardened configurations:
- [docs/compatibility.md](docs/compatibility.md) — Absolute backward compatibility contract and frozen interfaces.
- [docs/api.md](docs/api.md) — Exhaustive REST API specification and response schemas.
- [docs/configuration.md](docs/configuration.md) — Configuration guide and startup validation rules.
- [docs/hardening.md](docs/hardening.md) — Container runtime hardening, read-only rootfs, and dropped capabilities.
- [docs/releases.md](docs/releases.md) — Semantic versioning policy, upgrade paths, and instant rollback procedures.
- [docs/quality-baseline.md](docs/quality-baseline.md) — Measurable quality and performance baselines.

---
Expand Down
28 changes: 27 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Please **do not** report security vulnerabilities via public GitHub issues.
## Threat Model and Trust Boundaries

### 1. Local Container Security Boundary
- **Non-Root Execution**: The container runs under a dedicated unprivileged user (`toruser`, UID 999), never as `root` (UID 0).
- **Non-Root Execution**: The container runs under a dedicated unprivileged system user (`torproxy`), never as `root` (UID 0).
- **Control Port Authentication**: Tor's internal ControlPort (`127.0.0.1:9051`) requires authentication using an ephemeral filesystem cookie (`CookieAuthentication 1`). The cookie file permissions are restricted to the non-root container user (`0600`).
- **Loopback Internal Services**: Tor ControlPort listens strictly on loopback inside the container and is never exposed or published.

Expand All @@ -62,3 +62,29 @@ Please **do not** report security vulnerabilities via public GitHub issues.
- **No 100% Anonymity Guarantee**: Tor is an overlay network designed to conceal network routing. It does not strip application-level tracking cookies, browser fingerprints, or plaintext payloads.
- **Always Use HTTPS**: Plaintext HTTP traffic exiting a Tor exit relay can be monitored by the exit node operator. Always use `https://` for sensitive data.
- **Latency & Reliability**: Tor circuits incur variable latency and occasional connection drops. Circuit rotation (`/reconnect`) requests a fresh circuit from Tor via `SIGNAL NEWNYM`, but does not guarantee a distinct exit IP address if Tor's circuit pool is constrained.

---

## Vulnerability Management and Scanning Policy

To protect users against upstream vulnerabilities and supply chain compromises:

1. **Continuous Automated Scanning**:
- Every pull request and merge to `main` undergoes container vulnerability scanning using Trivy.
- Images are evaluated against standard CVE databases (Debian Security Advisories, NVD).
2. **Triage Severity SLA**:
- **Critical & High**: Addressed within 7 business days via base image update or upstream package bump.
- **Medium & Low**: Triaged during regular monthly release cycles.
3. **False Positive & Exception Process**:
- Any accepted risk or scanner false-positive must be documented in a `.trivyignore` file with an issue link, rationale, and expiration date.
4. **Base Image Updates**:
- The container builds upon `debian:bookworm-slim`. Security patches for `tor`, `privoxy`, `curl`, and `ca-certificates` are pulled directly from Debian security mirrors during build.

---

## Supply Chain Security and Provenance

- **Reproducible Multi-Arch Builds**: Images are built using Docker Buildx for `linux/amd64`, `linux/arm64`, and `linux/arm/v7`.
- **Software Bill of Materials (SBOM)**: Release builds generate automated SBOM metadata (SPDX format) embedded in the image attestation.
- **Provenance Attestations**: Docker Buildx generates SLSA provenance attestations (`mode=max`) guaranteeing build integrity from GitHub Actions.
- **Minimal Dependencies**: The image intentionally contains no node runtime, pip packages, or third-party binaries. Privoxy, Tor, and standard library Python 3 constitute the entire execution stack.
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ FROM debian:bookworm-slim

LABEL org.opencontainers.image.title="torproxy" \
org.opencontainers.image.description="A minimal HTTP proxy routed through Tor" \
org.opencontainers.image.version="1.2.0" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.source="https://github.com/hapheus/torproxy" \
org.opencontainers.image.url="https://github.com/hapheus/torproxy"
Expand Down
88 changes: 88 additions & 0 deletions docs/hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Container Runtime Hardening Guide

This document describes how to deploy `torproxy` with maximum runtime security and least privilege in production environments, without altering application behavior.

---

## 1. Security Architecture & Threat Boundaries

The `torproxy` container is designed with defensive depth:
- **Dedicated Non-Root User**: The image executes as the unprivileged user `torproxy` (UID/GID assigned during build).
- **Ephemeral Cookie Authentication**: The Tor ControlPort (`127.0.0.1:9051`) requires an unguessable 32-byte filesystem cookie authenticated per session.
- **Fail-Closed Forwarding**: Privoxy routes strictly to SOCKS5. If Tor is stopped, requests immediately fail with 500/503 and cannot leak to clearnet.

---

## 2. Hardening Measures

### A. Read-Only Root Filesystem (`read_only: true`)
Prevent attackers or rogue processes from modifying system binaries or writing persistent malware by mounting the container root filesystem as read-only.

`torproxy` only requires temporary write access to four locations, which should be mounted as memory-backed `tmpfs`:
- `/tmp`: Used by startup scripts for rendering configuration templates (`privoxy.config`, `torrc`).
- `/run`: Standard Linux runtime directory.
- `/var/lib/tor`: Tor ephemeral state, circuit data, and authentication cookie.
- `/var/log/privoxy`: Privoxy runtime logging.

### B. Dropping Linux Capabilities (`cap_drop: [ALL]`)
`torproxy` does not require any Linux capabilities (such as `CAP_NET_ADMIN`, `CAP_SYS_ADMIN`, or `CAP_CHOWN`). All capabilities can be dropped safely.

### C. Preventing Privilege Escalation (`no-new-privileges:true`)
Ensures that container processes cannot gain additional privileges via setuid or setgid binaries.

---

## 3. Production Hardened Docker Compose Configuration

The recommended production configuration drops all kernel capabilities and prohibits privilege escalation (available in [`examples/hardened-production/docker-compose.yml`](../examples/hardened-production/docker-compose.yml)):

```yaml
services:
torproxy:
image: hapheus/torproxy:latest
container_name: torproxy-hardened
restart: unless-stopped

# 1. Prevent privilege escalation
security_opt:
- no-new-privileges:true

# 2. Drop all Linux kernel capabilities
cap_drop:
- ALL

# 3. Bind published ports strictly to host loopback interface
ports:
- "127.0.0.1:8118:8118"
- "127.0.0.1:9050:9050"
- "127.0.0.1:8080:8080"
```

> [!NOTE]
> **Read-Only Root Filesystem (`read_only: true`)**:
> If your container runtime supports user-owned tmpfs mounts (such as Podman or Docker Compose with `tmpfs: [ /tmp, /run, /var/log/privoxy, /var/lib/tor:uid=...,gid=... ]`), you can additionally enable `read_only: true`. Tor requires `/var/lib/tor` to have mode `0700` owned by the container user (`torproxy`). For universal compatibility across standard Docker installations, `cap_drop: [ALL]` and `no-new-privileges: true` provide complete capability containment.

---

## 4. Verification

After starting the hardened container, verify that security constraints are active:

```bash
# 1. Verify read-only rootfs (should return "Read-only file system" error)
docker exec torproxy-hardened touch /etc/test.txt

# 2. Verify non-root execution (should be > 0)
docker exec torproxy-hardened id -u

# 3. Verify dropped capabilities
docker inspect --format '{{.HostConfig.CapDrop}}' torproxy-hardened
# Expected output: [ALL]

# 4. Verify no-new-privileges
docker inspect --format '{{.HostConfig.SecurityOpt}}' torproxy-hardened
# Expected output: [no-new-privileges:true]

# 5. Verify proxy functionality
curl --proxy http://127.0.0.1:8118 https://check.torproject.org/api/ip
```
Loading
Loading