From c9c950de1bf7de9b8357c70e25710812767721e9 Mon Sep 17 00:00:00 2001 From: Pigway Date: Sun, 11 Oct 2026 01:13:55 +0800 Subject: [PATCH] Prepare public repository privacy and trusted runner gates --- .github/workflows/verify.yml | 14 ++++++++------ README.md | 2 +- docs/PUBLICATION.md | 20 ++++++++++++++++++++ docs/deployment/GITEA_CD.md | 18 +++++++++--------- scripts/production-deploy.py | 13 ++++++++++++- tests/test_publication_security.py | 23 +++++++++++++++++++++++ 6 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 docs/PUBLICATION.md create mode 100644 tests/test_publication_security.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index aff60ea..a0a48b7 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -11,6 +11,8 @@ on: default: plan jobs: verify: + # The NAS Docker runner must never execute code from external forks. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest env: DOCKER_PIP_INDEX_URL: ${{ vars.DOCKER_PIP_INDEX_URL }} @@ -56,12 +58,12 @@ jobs: PROD_PROJECT: ${{ vars.PROD_PROJECT }} PROD_CONTAINER: ${{ vars.PROD_CONTAINER }} PROD_IMAGE_REPO: ${{ vars.PROD_IMAGE_REPO }} - PROD_DATA_DIR: ${{ vars.PROD_DATA_DIR }} - PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }} - PROD_PHOTOS_DIR: ${{ vars.PROD_PHOTOS_DIR }} - SYNOLOGY_PHOTOS_URL: ${{ vars.SYNOLOGY_PHOTOS_URL }} - SYNOLOGY_PHOTOS_CERT_SHA256: ${{ vars.SYNOLOGY_PHOTOS_CERT_SHA256 }} - PROD_WEB_BIND: ${{ vars.PROD_WEB_BIND }} + PROD_DATA_DIR: ${{ secrets.PROD_DATA_DIR }} + PROD_CONFIG_DIR: ${{ secrets.PROD_CONFIG_DIR }} + PROD_PHOTOS_DIR: ${{ secrets.PROD_PHOTOS_DIR }} + SYNOLOGY_PHOTOS_URL: ${{ secrets.SYNOLOGY_PHOTOS_URL }} + SYNOLOGY_PHOTOS_CERT_SHA256: ${{ secrets.SYNOLOGY_PHOTOS_CERT_SHA256 }} + PROD_WEB_BIND: ${{ secrets.PROD_WEB_BIND }} PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }} PROD_STATE_VOLUME: ${{ vars.PROD_STATE_VOLUME }} PROD_MOCK: ${{ vars.PROD_MOCK }} diff --git a/README.md b/README.md index aa7d71e..a4fba39 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ to 25M pixels/20 MiB. This release uses a single worker and is not multi-replica Gallery reads have no pagination because upstream documents none. Web UI hides secrets and lazy-loads thumbnails; generating a first thumbnail fetches one original. Scoped client provisioning and bounded signed webhook delivery are available in Connections. Docker build/NAS -status is recorded honestly in docs/ACCEPTANCE.md. +results should be checked in the Actions run for the relevant commit. ## Roadmap diff --git a/docs/PUBLICATION.md b/docs/PUBLICATION.md new file mode 100644 index 0000000..97fef19 --- /dev/null +++ b/docs/PUBLICATION.md @@ -0,0 +1,20 @@ +# Public repository and deployment privacy + +Runtime keys, passwords, sessions, private photos, databases and local `.env` files +must remain outside Git. Examples use documentation-only addresses and identities. +Personal deployment reports and screenshots are excluded from public history. + +The NAS Docker runner accepts trusted repository events only. External fork PRs +are not executed on this runner; maintainers must review them before copying changes +to a trusted branch. All lint, type, test, audit and isolated Docker checks remain. + +Private deployment configuration is stored in Gitea Actions Secrets: +`PROD_DATA_DIR`, `PROD_CONFIG_DIR`, `PROD_PHOTOS_DIR`, `PROD_WEB_BIND`, +`SYNOLOGY_PHOTOS_URL`, `SYNOLOGY_PHOTOS_CERT_SHA256`, and `PRODUCTION_ENV`. +Project/image names, ports, enable flags and mirror settings remain Variables. +Never print complete Docker inspect environment or secret values. + +Publishing requires scanning all refs, removing private historic files, pruning +unreachable remote Git objects and archiving/removing historical Actions logs. +A private original mirror is retained outside the repository. Existing clones must +reclone after the privacy history rewrite; do not merge the original history back. diff --git a/docs/deployment/GITEA_CD.md b/docs/deployment/GITEA_CD.md index 64d9022..1a9fbc7 100644 --- a/docs/deployment/GITEA_CD.md +++ b/docs/deployment/GITEA_CD.md @@ -9,8 +9,8 @@ host bind/port. SQLite/images/generated/cache live under `/data`; accounts, vendor sessions and configuration live under `/config`. Neither is disposable. The optional photos mount is read-only and has supplementary group access. -Historical repository receipts mention `/volume2/docker/pigway-frame-flow/data`, -`/volume2/docker/pigway-frame-flow/config`, `/volume1/photo`, port 18787, and +Historical repository receipts mention `/volume1/docker/pigway-frame-flow/data`, +`/volume1/docker/pigway-frame-flow/config`, `/volume1/photo`, port 18787, and container/project `pigway-frame-flow`. These are **confirmation candidates**, not script defaults. The deploy action accepts only explicitly configured paths that match the existing running container on the Runner's Docker daemon. @@ -24,7 +24,7 @@ configured names and existing bind mounts, with `create_host_path: false`. ## Gitea configuration -Repository Settings → Actions → Variables (ordinary configuration): +Repository Settings → Actions configuration (private paths/addresses use Secrets): | Variable | Value to explicitly confirm | | --- | --- | @@ -75,7 +75,7 @@ Never upload that journal or a full Docker inspect as a public CI artifact. docker inspect pigway-frame-flow --format '{{json .Mounts}}' docker inspect pigway-frame-flow --format '{{json .HostConfig.PortBindings}} {{.HostConfig.NetworkMode}}' ``` - Confirm the Variables against these actual values, including photos. + Confirm the Variables/Secrets against these actual values, including photos. 2. Configure the Secret privately. Keep `PROD_DEPLOY_ENABLED=false`. 3. Manually run **Verify Frame Flow** on `main`, `deploy_action=plan`. Verify must pass first. Plan checks resource identity, paths/port, required @@ -174,15 +174,15 @@ The confirmed NAS residue is a `created`, never-started container with dead `/workspace/way/...` bind paths. It is not an adoption/rollback baseline. No migration from those paths is performed. Production Compose remains independently usable. -Set these repository Actions **Variables** (not Secrets): +Set the ordinary repository Actions Variables below; set paths and addresses as Secrets (see docs/PUBLICATION.md): ```text PROD_DEPLOY_ENABLED=true PROD_PROJECT=pigway-frame-flow PROD_CONTAINER=pigway-frame-flow PROD_IMAGE_REPO=pigway-frame-flow -PROD_DATA_DIR=/volume2/docker/pigway-frame-flow/data -PROD_CONFIG_DIR=/volume2/docker/pigway-frame-flow/config +PROD_DATA_DIR=/volume1/docker/pigway-frame-flow/data +PROD_CONFIG_DIR=/volume1/docker/pigway-frame-flow/config PROD_WEB_BIND=192.0.2.10 PROD_WEB_PORT=18787 PROD_STATE_VOLUME=pigway-frame-flow-cd-state @@ -262,7 +262,7 @@ as an Actions artifact. First bootstrap has no prior healthy version to roll bac No NAS bootstrap is executed merely by committing this implementation. 3. Require `BOOTSTRAP VERIFIED` in the deploy log. In Container Manager confirm container `pigway-frame-flow` is running/healthy, user 10333, restart unless-stopped, - exact `/volume2/docker/pigway-frame-flow/{data,config}` mounts and + exact `/volume1/docker/pigway-frame-flow/{data,config}` mounts and `192.0.2.10:18787` binding (not loopback). 4. Open `http://192.0.2.10:18787/api/v1/version`; its `commit` must equal the full workflow checkout commit. Check `/ready` returns HTTP 200. The immutable image tag @@ -305,7 +305,7 @@ not every possible external download. No Runner or NAS network configuration is ### Synology Photos dependencies For a production photo source using Synology Photos person filtering, configure -`SYNOLOGY_PHOTOS_URL` and `SYNOLOGY_PHOTOS_CERT_SHA256` as Gitea Variables. The +`SYNOLOGY_PHOTOS_URL` and `SYNOLOGY_PHOTOS_CERT_SHA256` as Gitea Secrets. The certificate fingerprint must match the configured HTTPS endpoint. Keep the Photos session in the persistent `/config` directory; never put session credentials in Variables or repository files. diff --git a/scripts/production-deploy.py b/scripts/production-deploy.py index eaf5c31..5876dfa 100644 --- a/scripts/production-deploy.py +++ b/scripts/production-deploy.py @@ -37,7 +37,18 @@ class Deploy: self.lock_id = None self.state = None self.old_image = None - self.secret_values = [] + self.secret_values = [ + self.env[key] + for key in ( + "PROD_DATA_DIR", + "PROD_CONFIG_DIR", + "PROD_PHOTOS_DIR", + "PROD_WEB_BIND", + "SYNOLOGY_PHOTOS_URL", + "SYNOLOGY_PHOTOS_CERT_SHA256", + ) + if self.env.get(key) + ] self.docker = self.env.get("DOCKER_BIN", "docker") def run(self, *args, data=None, check=True): diff --git a/tests/test_publication_security.py b/tests/test_publication_security.py new file mode 100644 index 0000000..fc7099e --- /dev/null +++ b/tests/test_publication_security.py @@ -0,0 +1,23 @@ +from pathlib import Path + + +def test_external_forks_do_not_execute_on_nas_runner(): + workflow = Path(".github/workflows/verify.yml").read_text() + assert ( + "github.event.pull_request.head.repo.full_name == github.repository" in workflow + ) + assert "needs: verify" in workflow + + +def test_private_production_configuration_uses_secrets(): + workflow = Path(".github/workflows/verify.yml").read_text() + for key in ( + "PROD_DATA_DIR", + "PROD_CONFIG_DIR", + "PROD_PHOTOS_DIR", + "PROD_WEB_BIND", + "SYNOLOGY_PHOTOS_URL", + "SYNOLOGY_PHOTOS_CERT_SHA256", + ): + assert "${{ secrets." + key + " }}" in workflow + assert "${{ vars." + key + " }}" not in workflow