diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 590adea..b3ad039 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -16,6 +16,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Validate compose example + run: docker compose --file compose.example.yaml config --quiet + - name: Run tests run: | if [ -f docker-compose.test.yml ]; then diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..e3c768e --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,197 @@ +# Improvement Plan + +This plan prioritizes user safety and a working release pipeline before public +portfolio polish. The project already has useful adoption evidence: more than +5,800 Docker Hub pulls, external issues, and external contributions. + +Security-sensitive findings should be fixed and released before detailed +exploit scenarios are discussed in public issues. + +## P0 - Release blockers + +### Restore reproducible image builds + +- [ ] Replace the unavailable Seafile APT repository used in `Dockerfile`. +- [ ] Install a supported Seafile CLI version from an official source. +- [ ] Pin the version and verify its checksum or signature during the build. +- [ ] Remove `apt-key`, which is deprecated and trusts keys globally. +- [ ] Add a smoke check for `seaf-cli --version` to the image build. +- [ ] Scan the resulting image and review every high or critical finding. + +Acceptance criteria: + +- A clean `docker build --no-cache .` succeeds in CI. +- The installed Seafile version is explicit and current. +- A missing or invalid artifact signature fails the build. + +### Remove shell and path injection risks + +- [ ] Pass subprocess arguments as arrays instead of joining values into shell + command strings in `dsc/client.py` and `dsc/misc.py`. +- [ ] Replace `su -c` with a mechanism such as `runuser --` that does not need a + shell. +- [ ] Treat server-provided library names as untrusted input. +- [ ] Resolve every target directory and prove that it remains under + `/dsc/seafile` before creating or writing it. +- [ ] Define how duplicate, absolute, empty, and traversal-style library names + are handled. + +Acceptance criteria: + +- Tests cover quotes, command substitutions, separators, newlines, absolute + paths, parent traversal, Unicode, and colliding library names. +- No configuration or server value can execute a command or escape the data + directory. + +### Support secrets safely + +- [ ] Implement `PASSWORD_FILE` for Docker Compose and Swarm secrets. +- [ ] Add token-based authentication and `TOKEN_FILE` if supported by the + installed Seafile CLI. +- [ ] Define precedence and reject conflicting secret sources. +- [ ] Ensure secrets are absent from application logs. +- [ ] Document any upstream limitation that exposes a secret in process + arguments. + +Acceptance criteria: + +- A container can authenticate with a mounted file under `/run/secrets`. +- Tests reject conflicting inputs and verify newline handling. +- Logs never contain the password or token. + +### Correct environment parsing and defaults + +- [ ] Replace direct `os.getenv()` assignments in `start.py` with typed parsers. +- [ ] Preserve the documented defaults for upload limit, download limit, and + delete-confirm threshold when variables are absent. +- [ ] Validate UID, GID, limits, threshold, and booleans before daemon startup. +- [ ] Reject negative and malformed numeric values with useful messages. +- [ ] Do not pass `None` as a Seafile configuration value. + +Acceptance criteria: + +- Table-driven tests cover unset, zero, valid, negative, and malformed values. +- A default container receives exactly the defaults documented in the README. + +### Make startup and shutdown deterministic + +- [ ] Check every important subprocess return code. +- [ ] Add timeouts to daemon startup, shutdown, and readiness polling. +- [ ] Handle `SIGTERM` and `SIGINT` and stop the daemon in `finally`. +- [ ] Return a nonzero container exit code on configuration or daemon failure. +- [ ] Produce actionable errors instead of waiting forever. + +Acceptance criteria: + +- A daemon that never becomes ready causes a bounded failure. +- `docker stop` performs a graceful shutdown within Docker's timeout. +- Failure-injection tests cover command errors and interrupted startup. + +### Add real tests + +- [ ] Add unit tests for configuration parsing, command construction, library + resolution, path validation, retries, and status parsing. +- [ ] Add regression tests for externally reported password and deletion + threshold issues. +- [ ] Add a container smoke test for imports, installed CLI version, startup, + and shutdown using controlled fakes. +- [ ] Keep image construction as a separate CI job so packaging failures are + distinguishable from application failures. + +Acceptance criteria: + +- CI fails on a deliberate parsing, quoting, or path-safety regression. +- Application tests run without requiring a live production server. + +### Repair and validate the quick start + +- [x] Change the Compose example to mapping syntax so quote characters are not + passed as part of `LIBRARY_ID`. +- [x] Pin the example to a versioned image instead of an implicit latest tag. +- [x] Add a checked-in `compose.example.yaml`. +- [x] Validate it with `docker compose config` in CI. + +## P1 - Reliability and supply chain + +### Networking and protocol behavior + +- [ ] Add explicit connect and read timeouts to all HTTP requests. +- [ ] Define a bounded retry budget with backoff and jitter. +- [ ] Handle authentication, rate limits, malformed responses, and retryable + server errors separately. +- [ ] Make TLS configuration consistent between Python requests and Seafile. +- [ ] Support a custom CA bundle; keep certificate disabling as a last resort. +- [ ] Prefer structured JSON output from `seaf-cli` over parsing display text. +- [ ] Reject ambiguous library names and an empty requested library set. + +### Runtime hardening + +- [ ] Run the long-lived process as the configured non-root user after setup. +- [ ] Reject UID or GID zero unless a documented use case requires it. +- [ ] Add a health check for the daemon and RPC endpoint. +- [ ] Handle temporary RPC failures and daemon restarts without corrupting + state. +- [ ] Document supported CPU architectures instead of implying multi-platform + support. + +### CI and release pipeline + +- [ ] Replace `actions/checkout@v2` and `docker/build-push-action@v1`. +- [ ] Set workflow permissions explicitly to `contents: read` by default. +- [ ] Use a scoped Docker Hub access token. +- [ ] Build a release image once and attach all version and `latest` tags to + the same manifest digest. +- [ ] Add OCI labels, an SBOM, provenance, and image signing. +- [ ] Add dependency updates for Python, Docker, and GitHub Actions. +- [ ] Run dependency and image scans on pull requests and on a schedule. + +Acceptance criteria: + +- Every tag for one release resolves to the same manifest digest. +- Pull requests cannot access publication credentials. +- The current dependency update pull request passes all checks. + +## P2 - Maintainability and presentation + +### Simplify the implementation + +- [ ] Separate configuration, subprocess execution, library resolution, and + status monitoring so each can be tested independently. +- [ ] Remove unused imports and unnecessary dependencies. +- [ ] Add type annotations and static checks at external-input boundaries. +- [ ] Make logging configurable and avoid forced dependency debug logs. +- [ ] Add `.dockerignore` and reduce unnecessary image packages. + +### Establish a visible maintenance process + +- [ ] Publish GitHub Releases for future tags with compatibility and rollback + notes. +- [ ] Add `CHANGELOG.md`, `SECURITY.md`, and concise contribution guidance. +- [ ] Add issue templates that request image, client, and server versions plus + sanitized logs. +- [ ] Triage stale issues and document the status of Docker secret support. +- [ ] Enable branch protection and require passing CI before merge. + +### Improve the public project page + +- [ ] Link directly to the Docker Hub image near the top of `README.md`. +- [ ] Add CI, release, Docker pulls, and license badges after the checks are + trustworthy. +- [ ] Add a short architecture and process-lifecycle section. +- [ ] Add a tested compatibility matrix. +- [ ] Explain deletion behavior, volume ownership, state loss, backup + expectations, and recovery. +- [ ] Fix example inconsistencies and typographical errors. + +## Release gate + +Do not publish the next image until all of the following are true: + +- [ ] A clean, reproducible build uses a supported Seafile client. +- [ ] External values cannot reach a shell or escape the data directory. +- [ ] Secret-file or token authentication is covered by tests. +- [ ] Startup, retries, and shutdown are bounded and observable. +- [ ] Unit and container smoke tests pass. +- [ ] The steady-state process is non-root and has a useful health check. +- [ ] Release tags point to one scanned and signed image digest. +- [ ] The README quick start succeeds from a clean environment. diff --git a/README.md b/README.md index 2251650..fef1e78 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,24 @@ # docker-seafile-client Docker image for [Seafile CLI client](https://help.seafile.com/syncing_client/linux-cli/). +Image on Docker Hub: + [snegov/seafile-client](https://hub.docker.com/r/snegov/seafile-client). + ### Docker-compose example: +Also available as a ready-to-use [`compose.example.yaml`](compose.example.yaml). + ```yaml services: seafile-client: restart: always - image: snegov/seafile-client + image: snegov/seafile-client:0.0.9 environment: - - LIBRARY_ID="79867cbf-2944-488d-9105-852463ecdf9e:my_library" - - SERVER_HOST=seafile.example.com - - USERNAME=user - - PASSWORD=password - - SEAFILE_UID=1000 - - SEAFILE_GID=100 + LIBRARY_ID: "79867cbf-2944-488d-9105-852463ecdf9e:my_library" + SERVER_HOST: seafile.example.com + USERNAME: user + PASSWORD: password + SEAFILE_UID: 1000 + SEAFILE_GID: 100 hostname: dsc volumes: - /home/johndow/seafile:/dsc/seafile diff --git a/compose.example.yaml b/compose.example.yaml new file mode 100644 index 0000000..3254160 --- /dev/null +++ b/compose.example.yaml @@ -0,0 +1,19 @@ +services: + seafile-client: + restart: always + image: snegov/seafile-client:0.0.9 + environment: + LIBRARY_ID: "79867cbf-2944-488d-9105-852463ecdf9e:my_library" + SERVER_HOST: seafile.example.com + USERNAME: user + PASSWORD: password + SEAFILE_UID: 1000 + SEAFILE_GID: 100 + hostname: dsc + volumes: + - /home/johndow/seafile:/dsc/seafile + - sync-data:/dsc/seafile-data + container_name: seafile-client + +volumes: + sync-data: