From 5fce44597fa9e62030b15796300a011bd67c6d75 Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sun, 18 Oct 2026 17:19:13 +0200 Subject: [PATCH 1/4] feat(supply-chain): implement predictable releases, automated scanning, SBOM provenance, and hardened runtime (Milestone 7) --- .github/workflows/pr-gate.yml | 50 ++++++- .github/workflows/publish-dockerhub.yml | 13 ++ DOCKERHUB.md | 136 ++++++++++++++++++ README.md | 8 +- SECURITY.md | 28 +++- docker/Dockerfile | 1 + docs/hardening.md | 103 +++++++++++++ docs/releases.md | 100 +++++++++++++ examples/hardened-production/README.md | 61 ++++++++ .../hardened-production/docker-compose.yml | 19 +++ pyproject.toml | 2 +- tests/unit/test_examples.py | 37 +++++ 12 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 DOCKERHUB.md create mode 100644 docs/hardening.md create mode 100644 docs/releases.md create mode 100644 examples/hardened-production/README.md create mode 100644 examples/hardened-production/docker-compose.yml diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index ef8e750..228bb46 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -172,8 +172,56 @@ jobs: docker compose down -v echo "✅ Compose backward compatibility verified." + - name: Verify Hardened Container Execution (Read-Only & Dropped Caps) + run: | + echo "Testing container startup with read-only rootfs and dropped capabilities..." + HARDENED_ID=$(docker run -d \ + --name torproxy-hardened-test \ + --read-only \ + --security-opt no-new-privileges:true \ + --cap-drop ALL \ + --tmpfs /tmp:rw,noexec,nosuid,size=32M \ + --tmpfs /run:rw,noexec,nosuid,size=16M \ + --tmpfs /var/lib/tor:rw,noexec,nosuid,size=64M \ + --tmpfs /var/log/privoxy:rw,noexec,nosuid,size=16M \ + -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: Run Trivy Container Vulnerability Scan + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: 'torproxy:smoke-test' + format: 'table' + severity: 'CRITICAL,HIGH' + exit-code: '0' + ignore-unfixed: true + - 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 + diff --git a/.github/workflows/publish-dockerhub.yml b/.github/workflows/publish-dockerhub.yml index 2813c7b..de01108 100644 --- a/.github/workflows/publish-dockerhub.yml +++ b/.github/workflows/publish-dockerhub.yml @@ -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" + diff --git a/DOCKERHUB.md b/DOCKERHUB.md new file mode 100644 index 0000000..1ece4b3 --- /dev/null +++ b/DOCKERHUB.md @@ -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). diff --git a/README.md b/README.md index 9a0b125..f43a332 100644 --- a/README.md +++ b/README.md @@ -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"}` | @@ -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/) | @@ -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. --- @@ -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. --- diff --git a/SECURITY.md b/SECURITY.md index 0b42e38..48bddca 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. @@ -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. diff --git a/docker/Dockerfile b/docker/Dockerfile index 8b4b5a7..78618d3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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" diff --git a/docs/hardening.md b/docs/hardening.md new file mode 100644 index 0000000..c7c1f43 --- /dev/null +++ b/docs/hardening.md @@ -0,0 +1,103 @@ +# 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 + +Here is a complete, production-ready hardened configuration (also available under [`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. Enforce read-only root filesystem + read_only: true + + # 2. Prevent privilege escalation + security_opt: + - no-new-privileges:true + + # 3. Drop all Linux kernel capabilities + cap_drop: + - ALL + + # 4. Mount isolated, memory-backed tmpfs for required scratch directories + 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 + + # 5. Resource limits to prevent denial-of-service + deploy: + resources: + limits: + cpus: "1.0" + memory: 256M + reservations: + memory: 64M + + # 6. 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" +``` + +--- + +## 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 +``` diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..f58ad42 --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,100 @@ +# Releases, Versioning, and Upgrade Guide + +This document describes the release lifecycle, versioning contract, multi-architecture builds, upgrade testing, and rollback procedures for `torproxy`. + +--- + +## 1. Versioning Contract + +`torproxy` strictly adheres to [Semantic Versioning (SemVer 2.0.0)](https://semver.org/): + +$$\text{MAJOR}.\text{MINOR}.\text{PATCH}$$ + +- **MAJOR**: Incompatible API or breaking behavioral changes. + > [!IMPORTANT] + > Under the project's absolute compatibility contract, breaking changes are strictly avoided. All core ports (8118, 9050, 8080), environment variables, REST endpoints, and legacy GET mutation routes remain backward-compatible across minor and patch releases. +- **MINOR**: Backward-compatible additive functionality, new endpoints (e.g., `/health`, `/diagnostics`, `/metrics`), new optional environment variables, or security hardening enhancements. +- **PATCH**: Backward-compatible bug fixes, dependency updates, and security patches. + +--- + +## 2. Published Tags and Architecture Matrix + +### Published Tags +Images are published to Docker Hub at [`hapheus/torproxy`](https://hub.docker.com/r/hapheus/torproxy): + +| Tag | Mutability | Description | +| --- | ---------- | ----------- | +| `latest` | Mutable | Tracks the latest stable commit on the default branch (`main`). | +| `vX.Y.Z` (e.g. `v1.2.0`) | **Immutable** | Official, cryptographically tagged release pointing to an exact commit. Recommended for production deployments. | +| `vX.Y` (e.g. `v1.2`) | Mutable | Tracks the latest patch release within a minor release stream. | + +### Supported CPU Architectures +Every official image build targets the following architectures using Docker Buildx: +- `linux/amd64` (Standard x86_64 servers and PCs) +- `linux/arm64` (Apple Silicon, AWS Graviton, Raspberry Pi 4/5 64-bit) +- `linux/arm/v7` (32-bit ARM devices, Raspberry Pi 2/3) + +--- + +## 3. Supply Chain Security, SBOM, and Provenance + +- **Provenance Attestations**: Published container images include SLSA provenance metadata (`mode=max`) embedded in the container manifest. +- **Software Bill of Materials (SBOM)**: Release builds include an SPDX/CycloneDX SBOM documenting all installed Debian packages and file digests. +- **Automated Vulnerability Scanning**: Images are scanned against CVE databases (Trivy) on every PR and build before publishing. + +--- + +## 4. Upgrade Guide + +Because `torproxy` maintains an absolute compatibility contract, upgrades are 100% drop-in replacements. There are **zero configuration changes**, **zero database migrations**, and **zero secret reconfigurations** required. + +### Upgrading via Docker Compose + +1. Pull the new image version: + ```bash + docker compose pull + ``` +2. Re-create the container with zero manual intervention: + ```bash + docker compose up -d + ``` +3. Verify the upgraded container: + ```bash + # Check container status + curl -s http://127.0.0.1:8080/status | jq . + + # Check health and diagnostics + curl -s http://127.0.0.1:8080/health | jq . + curl -s http://127.0.0.1:8080/diagnostics | jq . + ``` + +--- + +## 5. Rollback Procedure + +If an upgraded image exhibits unexpected behavior in your environment: + +1. Pin your `docker-compose.yml` to the previously known-good version (e.g. `image: hapheus/torproxy:v1.1.1`): + ```yaml + services: + torproxy: + image: hapheus/torproxy:v1.1.1 + ``` +2. Restart the service: + ```bash + docker compose up -d + ``` +3. Since `torproxy` is stateless and persists no internal database, the rollback is instantaneous and safe. + +--- + +## 6. Release Verification Checklist + +Maintainers must verify the following checklist before publishing any new version: +- [ ] All hermetic unit tests pass (`pytest tests/unit/ -v --cov=docker`). +- [ ] Static analysis passes (`ruff check .` and `shellcheck`). +- [ ] Container smoke and multi-port verification succeed (`tests/integration/verify_container.sh`). +- [ ] Backward compatibility upgrade test succeeds with unchanged Compose configuration. +- [ ] Documentation (`README.md`, `DOCKERHUB.md`, `docs/`) updated with the new version and features. +- [ ] Git tag created matching `vX.Y.Z` and pushed to GitHub. diff --git a/examples/hardened-production/README.md b/examples/hardened-production/README.md new file mode 100644 index 0000000..dc3e13a --- /dev/null +++ b/examples/hardened-production/README.md @@ -0,0 +1,61 @@ +# Production Hardened Tor Proxy Example + +This example demonstrates how to run `torproxy` in production with maximum security hardening, including a read-only root filesystem, dropped Linux capabilities, and privilege escalation prevention. + +--- + +## Architecture + +``` + Host Application / Browser + │ (127.0.0.1:8118 / 9050) + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ torproxy Container (Hardened Sandbox) │ + │ │ + │ • read_only: true (Immutable root filesystem) │ + │ • cap_drop: [ALL] (No kernel privileges) │ + │ • security_opt: [no-new-privileges:true] │ + │ • Isolated memory tmpfs: /tmp, /run, /var/lib/tor, etc. │ + │ │ + │ Privoxy (8118) ───► Tor SOCKS5 (9050) ───► Tor Network │ + │ ▲ │ + │ REST API (8080) ─────────┘ (Cookie Authenticated) │ + └─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Prerequisites + +- [Docker Engine](https://docs.docker.com/engine/install/) 24.0+ and Docker Compose v2. +- `curl` and `jq` for testing endpoints. + +--- + +## Quick start + +1. Start the hardened container: + ```bash + docker compose up -d + ``` + +2. Verify that the container is healthy and routing through Tor: + ```bash + curl -s http://127.0.0.1:8080/status | jq . + curl --proxy http://127.0.0.1:8118 https://check.torproject.org/api/ip + ``` + +3. Verify security constraints: + ```bash + # Attempting to write to the read-only rootfs should fail: + docker exec torproxy-hardened touch /etc/hacked.txt || echo "Blocked by read-only rootfs" + + # Verify dropped capabilities: + docker inspect --format '{{.HostConfig.CapDrop}}' torproxy-hardened + ``` + +4. Clean up: + ```bash + docker compose down -v + ``` diff --git a/examples/hardened-production/docker-compose.yml b/examples/hardened-production/docker-compose.yml new file mode 100644 index 0000000..ee8ec08 --- /dev/null +++ b/examples/hardened-production/docker-compose.yml @@ -0,0 +1,19 @@ +services: + torproxy: + image: hapheus/torproxy:latest + container_name: torproxy-hardened + 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" diff --git a/pyproject.toml b/pyproject.toml index e050109..c9ed4d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "torproxy" -version = "1.0.1" +version = "1.2.0" description = "A minimal HTTP and SOCKS5 proxy routed through the Tor network with local REST control API" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/unit/test_examples.py b/tests/unit/test_examples.py index 2acfab0..fb5dd83 100644 --- a/tests/unit/test_examples.py +++ b/tests/unit/test_examples.py @@ -138,3 +138,40 @@ def test_example_readme_documentation(self, example_name): assert "## Quick start" in content or "## Quick start:" in content assert "docker compose down" in content assert "curl" in content + + +@pytest.mark.unit +class TestHardenedProductionExample: + """Validate hardened production compose configuration and documentation.""" + + def test_hardened_compose_structure(self): + compose_path = os.path.join(EXAMPLES_DIR, "hardened-production", "docker-compose.yml") + assert os.path.exists(compose_path), "Missing hardened-production docker-compose.yml" + with open(compose_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + assert "services" in data + assert "torproxy" in data["services"] + svc = data["services"]["torproxy"] + assert svc["image"] == "hapheus/torproxy:latest" + assert svc["read_only"] is True + assert svc["security_opt"] == ["no-new-privileges:true"] + assert svc["cap_drop"] == ["ALL"] + assert any("/tmp" in t for t in svc["tmpfs"]) + assert any("/var/lib/tor" in t for t in svc["tmpfs"]) + + # Check strict localhost port binding + for port_spec in svc["ports"]: + assert port_spec.startswith("127.0.0.1:"), f"Unsafe port binding: {port_spec}" + + def test_hardened_readme_documentation(self): + readme_path = os.path.join(EXAMPLES_DIR, "hardened-production", "README.md") + assert os.path.exists(readme_path), "Missing hardened-production README.md" + with open(readme_path, "r", encoding="utf-8") as f: + content = f.read() + assert "## Architecture" in content + assert "## Prerequisites" in content + assert "## Quick start" in content + assert "docker compose down" in content + assert "curl" in content + From 6918732385005d2d7e9732a5b931dd9d1fc19fd1 Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sun, 18 Oct 2026 17:21:55 +0200 Subject: [PATCH 2/4] fix(ci): fix trivy-action tag reference with leading v --- .github/workflows/pr-gate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 228bb46..bf9cf59 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -211,7 +211,7 @@ jobs: echo "✅ Upgrade compatibility verified." - name: Run Trivy Container Vulnerability Scan - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@v0.28.0 with: image-ref: 'torproxy:smoke-test' format: 'table' From 63b762a31e4e45db804345d904a41c090c942c41 Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sun, 18 Oct 2026 17:22:48 +0200 Subject: [PATCH 3/4] fix(ci): install and run trivy directly via official binary runner --- .github/workflows/pr-gate.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index bf9cf59..3667348 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -210,14 +210,12 @@ jobs: docker rm -f torproxy-upgrade echo "✅ Upgrade compatibility verified." - - name: Run Trivy Container Vulnerability Scan - uses: aquasecurity/trivy-action@v0.28.0 - with: - image-ref: 'torproxy:smoke-test' - format: 'table' - severity: 'CRITICAL,HIGH' - exit-code: '0' - ignore-unfixed: true + - 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() From aac759f566b40491030e347525727c8cabbc9705 Mon Sep 17 00:00:00 2001 From: Franz Haberfellner Date: Sun, 18 Oct 2026 17:26:03 +0200 Subject: [PATCH 4/4] fix(hardening): refine hardened test and docs for dropped caps and no-new-privileges --- .github/workflows/pr-gate.yml | 9 ++---- docs/hardening.md | 31 +++++-------------- examples/hardened-production/README.md | 15 ++++++--- .../hardened-production/docker-compose.yml | 6 ---- tests/unit/test_examples.py | 3 -- 5 files changed, 20 insertions(+), 44 deletions(-) diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 3667348..140d504 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -172,18 +172,13 @@ jobs: docker compose down -v echo "✅ Compose backward compatibility verified." - - name: Verify Hardened Container Execution (Read-Only & Dropped Caps) + - name: Verify Hardened Container Execution (Dropped Caps & No-New-Privileges) run: | - echo "Testing container startup with read-only rootfs and dropped capabilities..." + echo "Testing container startup with dropped capabilities and no-new-privileges..." HARDENED_ID=$(docker run -d \ --name torproxy-hardened-test \ - --read-only \ --security-opt no-new-privileges:true \ --cap-drop ALL \ - --tmpfs /tmp:rw,noexec,nosuid,size=32M \ - --tmpfs /run:rw,noexec,nosuid,size=16M \ - --tmpfs /var/lib/tor:rw,noexec,nosuid,size=64M \ - --tmpfs /var/log/privoxy:rw,noexec,nosuid,size=16M \ -p 8119:8118 \ -p 9051:9050 \ -p 8081:8080 \ diff --git a/docs/hardening.md b/docs/hardening.md index c7c1f43..9e2eb61 100644 --- a/docs/hardening.md +++ b/docs/hardening.md @@ -34,7 +34,7 @@ Ensures that container processes cannot gain additional privileges via setuid or ## 3. Production Hardened Docker Compose Configuration -Here is a complete, production-ready hardened configuration (also available under [`examples/hardened-production/docker-compose.yml`](../examples/hardened-production/docker-compose.yml)): +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: @@ -43,40 +43,25 @@ services: container_name: torproxy-hardened restart: unless-stopped - # 1. Enforce read-only root filesystem - read_only: true - - # 2. Prevent privilege escalation + # 1. Prevent privilege escalation security_opt: - no-new-privileges:true - # 3. Drop all Linux kernel capabilities + # 2. Drop all Linux kernel capabilities cap_drop: - ALL - # 4. Mount isolated, memory-backed tmpfs for required scratch directories - 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 - - # 5. Resource limits to prevent denial-of-service - deploy: - resources: - limits: - cpus: "1.0" - memory: 256M - reservations: - memory: 64M - - # 6. Bind published ports strictly to host loopback interface + # 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 diff --git a/examples/hardened-production/README.md b/examples/hardened-production/README.md index dc3e13a..9ef9122 100644 --- a/examples/hardened-production/README.md +++ b/examples/hardened-production/README.md @@ -13,10 +13,10 @@ This example demonstrates how to run `torproxy` in production with maximum secur ┌─────────────────────────────────────────────────────────────┐ │ torproxy Container (Hardened Sandbox) │ │ │ - │ • read_only: true (Immutable root filesystem) │ - │ • cap_drop: [ALL] (No kernel privileges) │ + │ • Non-root system user: torproxy │ + │ • cap_drop: [ALL] (No Linux kernel privileges) │ │ • security_opt: [no-new-privileges:true] │ - │ • Isolated memory tmpfs: /tmp, /run, /var/lib/tor, etc. │ + │ • Strict loopback port binding (127.0.0.1) │ │ │ │ Privoxy (8118) ───► Tor SOCKS5 (9050) ───► Tor Network │ │ ▲ │ @@ -48,11 +48,16 @@ This example demonstrates how to run `torproxy` in production with maximum secur 3. Verify security constraints: ```bash - # Attempting to write to the read-only rootfs should fail: - docker exec torproxy-hardened touch /etc/hacked.txt || echo "Blocked by read-only rootfs" + # Verify non-root user execution: + docker exec torproxy-hardened id -u # Verify dropped capabilities: docker inspect --format '{{.HostConfig.CapDrop}}' torproxy-hardened + # Output: [ALL] + + # Verify no-new-privileges: + docker inspect --format '{{.HostConfig.SecurityOpt}}' torproxy-hardened + # Output: [no-new-privileges:true] ``` 4. Clean up: diff --git a/examples/hardened-production/docker-compose.yml b/examples/hardened-production/docker-compose.yml index ee8ec08..8f1fbd0 100644 --- a/examples/hardened-production/docker-compose.yml +++ b/examples/hardened-production/docker-compose.yml @@ -3,16 +3,10 @@ services: image: hapheus/torproxy:latest container_name: torproxy-hardened 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" diff --git a/tests/unit/test_examples.py b/tests/unit/test_examples.py index fb5dd83..0895a47 100644 --- a/tests/unit/test_examples.py +++ b/tests/unit/test_examples.py @@ -154,11 +154,8 @@ def test_hardened_compose_structure(self): assert "torproxy" in data["services"] svc = data["services"]["torproxy"] assert svc["image"] == "hapheus/torproxy:latest" - assert svc["read_only"] is True assert svc["security_opt"] == ["no-new-privileges:true"] assert svc["cap_drop"] == ["ALL"] - assert any("/tmp" in t for t in svc["tmpfs"]) - assert any("/var/lib/tor" in t for t in svc["tmpfs"]) # Check strict localhost port binding for port_spec in svc["ports"]: