Skip to content
Closed
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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ python -m zensical build --clean --strict
python -I .github/scripts/site-check.py site
```

Two containers: `cashpilot-ui` (port 8080, web dashboard + earnings collection) and `cashpilot-worker` (port 8081, Docker agent requiring socket access).
Three long-running containers in `docker-compose.yml`: `cashpilot-ui` (port 8080, web dashboard + earnings collection), `cashpilot-worker` (port 8081, Docker agent) and `cashpilot-socket-proxy` (holds the host's Docker socket). Since v1.71.0 the worker has no Docker socket: it reaches Docker through the proxy's filtered socket (`DOCKER_HOST=unix:///run/cashpilot-docker/docker.sock`), and since v1.73.0 the proxy's owner isolation limits it to containers, volumes and networks that carry the worker's owner label (`cashpilot.deployed-by=worker`); images are not scoped. See `docs/security-defaults.md`. The Unraid templates and the TrueNAS app still mount the raw socket in the worker (`docs/app-stores.md`).

### Testing traps

Expand Down Expand Up @@ -114,7 +114,7 @@ CashPilot is split into two **always-separate** components. The UI never touches
| Component | Description |
|-----------|-------------|
| **CashPilot UI** | The single web dashboard. Collects all earnings centrally, shows global fleet view, manages workers. **Has NO Docker socket.** Can be hosted anywhere. Only ONE UI instance exists. |
| **CashPilot Worker** | Agent running on each server that has Docker. **Must have Docker socket access**. Manages local containers, reports status to UI via heartbeats. Has a minimal config page. |
| **CashPilot Worker** | Agent running on each server that has Docker. **Reaches Docker through the socket proxy** (`cashpilot-socket-proxy`) in the shipped compose files; the Unraid and TrueNAS listings still mount the raw socket. Manages local containers, reports status to UI via heartbeats. Has a minimal config page. |

Two separate Docker images:
- **`drumsergio/cashpilot`** — UI image: FastAPI, Jinja2, templates, static assets, collectors, APScheduler. **No Docker SDK.**
Expand All @@ -125,7 +125,7 @@ Two separate Docker images:
### Core Principles

1. **Separation of concerns.** UI handles: dashboard, earnings collection, scheduling, user auth. Workers handle: Docker container lifecycle, health reporting.
2. **Workers must be privileged.** A worker without Docker socket is useless.
2. **Workers need Docker, through the socket proxy.** A worker without Docker access is useless, but in the shipped compose files it never gets the raw socket: the proxy refuses privileged containers and binds of `/` or the socket, and limits the worker to containers, volumes and networks with its owner label. The Unraid and TrueNAS listings still give it the raw socket, which is root on the host (`docs/app-stores.md`).
3. **Single source of truth.** The UI instance is the only one that collects earnings and stores historical data.
4. **Earnings are never duplicated.** Since only the UI collects, there is no risk of double-counting.
5. **Workers are stateless satellites.** A worker knows which containers to keep running and the UI URL to report to.
Expand Down Expand Up @@ -204,7 +204,7 @@ filtered on `service` silently matched nothing in production. Use
## Key Rules
- Never hardcode credentials; all secrets via environment variables
- Docker images: `drumsergio/cashpilot` with semver tags, never `:latest`
- Worker requires Docker socket access (`/var/run/docker.sock`)
- In the shipped compose files the worker reaches Docker only through the socket proxy (`DOCKER_HOST=unix:///run/cashpilot-docker/docker.sock`) and never mounts `/var/run/docker.sock` (the proxy does, and so does the one-shot `cashpilot-migrate`)
- Always use bind mounts (not named volumes) on Unraid
- License: GPL-3.0

Expand Down
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ If CashPilot cannot write the key to disk at all, because `/data` is unwritable

**What about security?**

Every service CashPilot deploys runs in its own container with every Linux capability dropped (a few services add back only the ones they declare) and `--security-opt no-new-privileges` set, so a process inside cannot gain privileges. Containers cannot see your host filesystem beyond the volumes a service declares. They CAN reach your local network by default, because Docker does not block that; [Protecting Your Home Network](home-network-security.md) shows how to put them on a firewalled bridge that reaches the internet but not your LAN, router or host. That bridge does not cover a service on host networking (Mysterium, today); the guide says what to do about those. Service credentials are encrypted at rest using Fernet symmetric encryption. Only the worker container requires Docker socket access; the UI container has no privileged access.
Every service CashPilot deploys runs in its own container with every Linux capability dropped (a few services add back only the ones they declare) and `--security-opt no-new-privileges` set, so a process inside cannot gain privileges. Containers cannot see your host filesystem beyond the volumes a service declares. They CAN reach your local network by default, because Docker does not block that; [Protecting Your Home Network](home-network-security.md) shows how to put them on a firewalled bridge that reaches the internet but not your LAN, router or host. That bridge does not cover a service on host networking (Mysterium, today); the guide says what to do about those. Service credentials are encrypted at rest using Fernet symmetric encryption. The UI has no Docker access. With the shipped compose files the worker is never given the raw Docker socket: it reaches Docker through `cashpilot-socket-proxy`, which refuses privileged containers and binds of `/` or the socket, and limits the worker to containers, volumes and networks with its owner label ([security defaults](security-defaults.md)). The Unraid templates and the TrueNAS app still mount the raw socket in the worker, which is root on the host ([app stores](app-stores.md)).

That said, no setup is bulletproof. You are still running third-party software that routes external traffic through your network. Docker isolation significantly reduces the attack surface compared to running these services directly on your host, but it does not eliminate all risk. We recommend running CashPilot on a dedicated machine or VLAN, keeping Docker and your host OS up to date, and reviewing the open-source code of any service before deploying it.

Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ There is **no standalone mode**. Every server that runs Docker containers needs
## Design Principles

1. **Separation of concerns.** UI handles: dashboard, earnings collection, scheduling, user auth. Workers handle: Docker container lifecycle, health reporting. They never overlap.
2. **Workers must be privileged.** A worker without Docker socket is useless. If you don't need Docker management, just run the UI alone to track earnings.
2. **Workers need Docker, through the socket proxy.** A worker without Docker access is useless. With the shipped compose files it never gets the raw socket: it reaches Docker through `cashpilot-socket-proxy`. The Unraid and TrueNAS listings still mount the raw socket ([app stores](app-stores.md)). If you don't need Docker management, just run the UI alone to track earnings.
3. **Single source of truth.** The UI instance is the only one that collects earnings, stores historical data, and serves the dashboard. Workers never collect earnings.
4. **Earnings are never duplicated.** Since only the UI collects, there is no risk of the same account being counted twice.
5. **Workers are stateless satellites.** A worker knows which containers to keep running and the UI URL to report to. It persists only its own id and fleet key to local files (`.worker_id`, `.worker_key`; no database, no earnings data).
Expand Down
10 changes: 9 additions & 1 deletion tests/test_docs_say_the_socket_proxy_holds_the_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@

# Pages describing what the shipped compose file installs today. Changelogs,
# upgrade notes and research notes describe the past on purpose.
PAGES = [ROOT / "README.md", *sorted((ROOT / "docs").glob("*.md"))]
# CLAUDE.md is read by every agent that changes the code, so it counts too.
PAGES = [ROOT / "README.md", ROOT / "CLAUDE.md", *sorted((ROOT / "docs").glob("*.md"))]

STALE = [
r"\btwo (docker )?containers\b",
r"\bworker (itself )?(holds|mounts|touches|has) the docker socket\b",
r"\bonly the worker (holds|mounts|touches) the docker socket\b",
r"\bboth can command the docker socket\b",
r"\bdoes not keep the containers off your lan by itself\b",
r"\brequiring (docker )?socket access\b",
r"\b(must|requires?) (have )?docker socket access\b",
r"\bworkers must be privileged\b",
]

# Each entry: a page and a pattern that is true there, with the reason.
Expand Down Expand Up @@ -64,6 +68,10 @@ def test_no_page_says_the_worker_holds_the_socket(page: Path):
"A worker holds the Docker socket on its server.",
"Only the worker\ntouches the Docker socket.",
"It does not keep the containers off your LAN by itself.",
"cashpilot-worker (port 8081, Docker agent requiring socket access)",
"Agent running on each server. **Must have Docker socket access**.",
"- Worker requires Docker socket access (`/var/run/docker.sock`)",
"2. **Workers must be privileged.** A worker without Docker socket is useless.",
],
)
def test_the_check_catches_each_old_claim(text: str):
Expand Down
Loading