Parent epic: #413. Surfaced while diagnosing #612.
Problem
The dual-run quarantine mirror throws Access Denied writing to the quarantine bucket — the scoped key file-movement uses (metadata-svc) has no write policy for that bucket.
Evidence (live, Seq, 2026-07-16 06:12:28Z)
FileMovement WARN Quarantine mirror failed for /app/quarantine/..._advanced-game-narrative-toolbox-2nd.pdf — disk copy remains authoritative
Amazon.S3.AmazonS3Exception: Access Denied
Why it matters
It is non-fatal today (best-effort mirror; the disk copy stays authoritative, logged as a Warning), so quarantine still works on the filesystem. But the #529/#526 cutover removes the /app/quarantine bind — at which point quarantine writes have nowhere to go and fail outright. The uploads-batch-cutover-runbook.md (#529) already flagged exactly this:
Note: file-movement reuses metadata-svc today — widen that policy or (better) issue a dedicated movement-svc and update its stack env.
Fix (MinIO policy, no code)
Per the #529 batch table, the key file-movement uses needs rw on: ebooks, quarantine, dlls, assets, blogs. Either:
widen the metadata-svc policy to include those buckets, or
issue a dedicated movement-svc key with that policy and point spikersoft-file-movement's stack env at it (cleaner least-privilege split).
Then verify: EICAR upload → object appears in spiker/quarantine (the #529 Phase-2 verification step).
Blocks
#526 (quarantine bucket) and the #529 uploads-batch cutover.
Parent epic: #413. Surfaced while diagnosing #612.
## Problem
The dual-run **quarantine mirror throws `Access Denied` writing to the `quarantine` bucket** — the scoped key `file-movement` uses (`metadata-svc`) has no write policy for that bucket.
## Evidence (live, Seq, 2026-07-16 06:12:28Z)
```
FileMovement WARN Quarantine mirror failed for /app/quarantine/..._advanced-game-narrative-toolbox-2nd.pdf — disk copy remains authoritative
Amazon.S3.AmazonS3Exception: Access Denied
```
## Why it matters
It is **non-fatal today** (best-effort mirror; the disk copy stays authoritative, logged as a Warning), so quarantine still works on the filesystem. But the **#529/#526 cutover removes the `/app/quarantine` bind** — at which point quarantine writes have nowhere to go and fail outright. The `uploads-batch-cutover-runbook.md` (#529) already flagged exactly this:
> **Note:** file-movement reuses `metadata-svc` today — widen that policy or (better) issue a dedicated `movement-svc` and update its stack env.
## Fix (MinIO policy, no code)
Per the #529 batch table, the key file-movement uses needs **rw on: ebooks, quarantine, dlls, assets, blogs**. Either:
- widen the `metadata-svc` policy to include those buckets, or
- issue a dedicated `movement-svc` key with that policy and point `spikersoft-file-movement`'s stack env at it (cleaner least-privilege split).
Then verify: EICAR upload → object appears in `spiker/quarantine` (the #529 Phase-2 verification step).
## Blocks
#526 (quarantine bucket) and the #529 uploads-batch cutover.
Fix authored as the runbook-preferred dedicated key — two paired PRs up:
infra #126 (GATED): extends openbao/provision-minio-svc-users.sh with movement-svc (movement-svc-storage: rw ebooks/quarantine/dlls/assets/blogs — the exact #529 table row, DeleteObject included for the #499 purge; key → secret/ci/backend/minio/movement), plus two things this ticket's class of bug demanded: an attach-only widening of uploads-svc with ebooks-ro (#598's BookImageLocator would otherwise 403 silently) and a closing policy audit that prints accessKey → attached policies for all 8 svc users, so "does the key actually have the policy?" stops being unverifiable. Also swaps the file-movement stack env to movement-svc/MOVEMENT_S3_SECRET_KEY.
backend #415: file-movement workflow fetches secret/ci/backend/minio/movement (guard message points at the script).
on dreamstream1: export BAO_TOKEN=…; cd /mnt/infrastructure/openbao && sudo -E bash provision-minio-svc-users.sh (idempotent; existing users untouched; audit printed at the end)
merge backend #415 → next file-movement deploy picks up the key
verify + close this ticket: EICAR upload → object appears in spiker/quarantine
metadata-svc deliberately keeps its original scope (write ebooks, read uploads/ai-models) for extractor/embeddings/quiz — least privilege preserved.
**Fix authored as the runbook-preferred dedicated key — two paired PRs up:**
- **infra #126** (GATED): extends `openbao/provision-minio-svc-users.sh` with `movement-svc` (`movement-svc-storage`: rw ebooks/quarantine/dlls/assets/blogs — the exact #529 table row, DeleteObject included for the #499 purge; key → `secret/ci/backend/minio/movement`), plus two things this ticket's class of bug demanded: an attach-only widening of `uploads-svc` with `ebooks-ro` (#598's BookImageLocator would otherwise 403 silently) and a closing **policy audit** that prints accessKey → attached policies for all 8 svc users, so "does the key actually have the policy?" stops being unverifiable. Also swaps the file-movement stack env to `movement-svc`/`MOVEMENT_S3_SECRET_KEY`.
- **backend #415**: file-movement workflow fetches `secret/ci/backend/minio/movement` (guard message points at the script).
**Ops sequence (Joey / a SERVER-side session):**
1. merge infra #126
2. on dreamstream1: `export BAO_TOKEN=…; cd /mnt/infrastructure/openbao && sudo -E bash provision-minio-svc-users.sh` (idempotent; existing users untouched; audit printed at the end)
3. merge backend #415 → next file-movement deploy picks up the key
4. **verify + close this ticket:** EICAR upload → object appears in `spiker/quarantine`
`metadata-svc` deliberately keeps its original scope (write ebooks, read uploads/ai-models) for extractor/embeddings/quiz — least privilege preserved.
2026-07-18 provision run (Joey, dreamstream1) — MinIO side DONE, one recovery step left.
The audit output is the evidence this ticket wanted:
movement-svc: movement-svc-storage ← created + attached ✓
uploads-svc: ebooks-ro,uploads-rw ← #598 widening applied ✓
blogs-svc: blogs-svc-storage ← #522 write question SETTLED ✓
metadata-svc: ai-models-ro,metadata-ebooks-rw ← confirms this ticket's root cause:
no quarantine/dlls/assets/blogs anywhere in its scope
The one failure: the OpenBao write 403'd (permission denied on secret/ci/backend/minio/movement) — the BAO_TOKEN used lacked write on secret/data/ci/backend/minio/*. Since the script never prints secrets and MinIO secret keys are returned exactly once, movement-svc's generated secret is unrecoverable. That's the exact scenario rotate-minio-ci-keys.sh documents; recovery is one command with an admin-capable token:
export BAO_TOKEN=<token with write on secret/ci/backend/minio/*>
cd /mnt/infrastructure/openbao && ONLY_SVCS="movement" sudo -E bash rotate-minio-ci-keys.sh
(the rotate loop is generic — works with the current checkout; infra PR #128 adds movement to its default set)
Then: dispatch/redeploy the file-movement workflow (backend #415 is merged — it now fetches the movement path; until the rotate runs, its guard refuses loudly, which is the designed fail-safe). Close this ticket on: EICAR upload → object in spiker/quarantine.
**2026-07-18 provision run (Joey, dreamstream1) — MinIO side DONE, one recovery step left.**
The audit output is the evidence this ticket wanted:
```
movement-svc: movement-svc-storage ← created + attached ✓
uploads-svc: ebooks-ro,uploads-rw ← #598 widening applied ✓
blogs-svc: blogs-svc-storage ← #522 write question SETTLED ✓
metadata-svc: ai-models-ro,metadata-ebooks-rw ← confirms this ticket's root cause:
no quarantine/dlls/assets/blogs anywhere in its scope
```
**The one failure:** the OpenBao write 403'd (`permission denied` on `secret/ci/backend/minio/movement`) — the `BAO_TOKEN` used lacked write on `secret/data/ci/backend/minio/*`. Since the script never prints secrets and MinIO secret keys are returned exactly once, **movement-svc's generated secret is unrecoverable**. That's the exact scenario `rotate-minio-ci-keys.sh` documents; recovery is one command with an admin-capable token:
```
export BAO_TOKEN=<token with write on secret/ci/backend/minio/*>
cd /mnt/infrastructure/openbao && ONLY_SVCS="movement" sudo -E bash rotate-minio-ci-keys.sh
```
(the rotate loop is generic — works with the current checkout; infra PR #128 adds `movement` to its default set)
**Then:** dispatch/redeploy the file-movement workflow (backend #415 is merged — it now fetches the movement path; until the rotate runs, its guard refuses loudly, which is the designed fail-safe). **Close this ticket on:** EICAR upload → object in `spiker/quarantine`.
Board-sweep finding (2026-07-22): HALF-SHIPPED — the movement-svc cutover is fully merged (infra stack Storage__AccessKey=movement-svc + the file-movement workflow's MOVEMENT secret plumbing), but the RUNNING service still carries Storage__AccessKey=metadata-svc — the env change never rolled to the swarm. Dispatched the file-movement workflow just now to redeploy properly (Bao-fetched secret); once the service env shows movement-svc and a quarantine write succeeds, this closes. Will follow up after the rollout.
Board-sweep finding (2026-07-22): HALF-SHIPPED — the movement-svc cutover is fully merged (infra stack Storage__AccessKey=movement-svc + the file-movement workflow's MOVEMENT secret plumbing), but the RUNNING service still carries Storage__AccessKey=metadata-svc — the env change never rolled to the swarm. Dispatched the file-movement workflow just now to redeploy properly (Bao-fetched secret); once the service env shows movement-svc and a quarantine write succeeds, this closes. Will follow up after the rollout.
Audited against origin/master — all the code and provisioning landed; only the one-time key rotation remains. Staying open on that single step.
Committed and complete:
spikersoft-file-movement/docker-stack.yml:32-33 — Storage__AccessKey=movement-svc, Storage__SecretKey=${MOVEMENT_S3_SECRET_KEY}. The borrowed metadata-svc key is gone from the stack, and the header at :18-20 documents why (movement-svc gets rw on ebooks/quarantine/dlls/assets/blogs — the buckets metadata-svc's policy could not write).
openbao/provision-minio-svc-users.sh:20 provisions movement-svc with exactly that policy. The note at :47-50 records that this resolved the runbook's open decision the way it recommended, and that the stack env swap plus the backend workflow's secret/ci/backend/minio/movement path shipped in the paired PRs.
The recovery path is supported: both provision-minio-svc-users.sh:60 and rotate-minio-ci-keys.sh:55 honour ONLY_SVCS, so ONLY_SVCS="movement" ./rotate-minio-ci-keys.sh is a real, scoped command rather than an all-or-nothing rotation.
Remaining — one action, unrecorded: run ONLY_SVCS="movement" rotate-minio-ci-keys.sh with an admin-capable Bao token, so the secret/ci/backend/minio/movement path actually holds a key. Per comment 10415 the state was HALF-SHIPPED — merged stack env, running service still on metadata-svc — and no infra commit since then records the rotation. Git can't prove it either way; the settling evidence is a successful File Movement create_manifest against the quarantine bucket.
This is blocking more than it looks.#529's phase-0 items and #783's follow-up both name this rotation as outstanding, and #529's whole 7-service cutover sits behind it. Given the fix is a single scoped command against tooling that already exists, this is probably the highest ratio of unblocked-work to effort currently on the board.
Audited against `origin/master` — **all the code and provisioning landed; only the one-time key rotation remains.** Staying open on that single step.
**Committed and complete:**
- `spikersoft-file-movement/docker-stack.yml:32-33` — `Storage__AccessKey=movement-svc`, `Storage__SecretKey=${MOVEMENT_S3_SECRET_KEY}`. The borrowed `metadata-svc` key is gone from the stack, and the header at `:18-20` documents why (`movement-svc` gets rw on ebooks/quarantine/dlls/assets/blogs — the buckets `metadata-svc`'s policy could not write).
- `openbao/provision-minio-svc-users.sh:20` provisions `movement-svc` with exactly that policy. The note at `:47-50` records that this resolved the runbook's open decision the way it recommended, and that the stack env swap plus the backend workflow's `secret/ci/backend/minio/movement` path shipped in the paired PRs.
- The recovery path is supported: both `provision-minio-svc-users.sh:60` and `rotate-minio-ci-keys.sh:55` honour `ONLY_SVCS`, so `ONLY_SVCS="movement" ./rotate-minio-ci-keys.sh` is a real, scoped command rather than an all-or-nothing rotation.
**Remaining — one action, unrecorded:** run `ONLY_SVCS="movement" rotate-minio-ci-keys.sh` with an admin-capable Bao token, so the `secret/ci/backend/minio/movement` path actually holds a key. Per comment 10415 the state was HALF-SHIPPED — merged stack env, running service still on `metadata-svc` — and no infra commit since then records the rotation. Git can't prove it either way; the settling evidence is a successful File Movement `create_manifest` against the quarantine bucket.
**This is blocking more than it looks.** #529's phase-0 items and #783's follow-up both name this rotation as outstanding, and #529's whole 7-service cutover sits behind it. Given the fix is a single scoped command against tooling that already exists, this is probably the highest ratio of unblocked-work to effort currently on the board.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Parent epic: #413. Surfaced while diagnosing #612.
Problem
The dual-run quarantine mirror throws
Access Deniedwriting to thequarantinebucket — the scoped keyfile-movementuses (metadata-svc) has no write policy for that bucket.Evidence (live, Seq, 2026-07-16 06:12:28Z)
Why it matters
It is non-fatal today (best-effort mirror; the disk copy stays authoritative, logged as a Warning), so quarantine still works on the filesystem. But the #529/#526 cutover removes the
/app/quarantinebind — at which point quarantine writes have nowhere to go and fail outright. Theuploads-batch-cutover-runbook.md(#529) already flagged exactly this:Fix (MinIO policy, no code)
Per the #529 batch table, the key file-movement uses needs rw on: ebooks, quarantine, dlls, assets, blogs. Either:
metadata-svcpolicy to include those buckets, ormovement-svckey with that policy and pointspikersoft-file-movement's stack env at it (cleaner least-privilege split).Then verify: EICAR upload → object appears in
spiker/quarantine(the #529 Phase-2 verification step).Blocks
#526 (quarantine bucket) and the #529 uploads-batch cutover.
Fix authored as the runbook-preferred dedicated key — two paired PRs up:
openbao/provision-minio-svc-users.shwithmovement-svc(movement-svc-storage: rw ebooks/quarantine/dlls/assets/blogs — the exact #529 table row, DeleteObject included for the #499 purge; key →secret/ci/backend/minio/movement), plus two things this ticket's class of bug demanded: an attach-only widening ofuploads-svcwithebooks-ro(#598's BookImageLocator would otherwise 403 silently) and a closing policy audit that prints accessKey → attached policies for all 8 svc users, so "does the key actually have the policy?" stops being unverifiable. Also swaps the file-movement stack env tomovement-svc/MOVEMENT_S3_SECRET_KEY.secret/ci/backend/minio/movement(guard message points at the script).Ops sequence (Joey / a SERVER-side session):
export BAO_TOKEN=…; cd /mnt/infrastructure/openbao && sudo -E bash provision-minio-svc-users.sh(idempotent; existing users untouched; audit printed at the end)spiker/quarantinemetadata-svcdeliberately keeps its original scope (write ebooks, read uploads/ai-models) for extractor/embeddings/quiz — least privilege preserved.2026-07-18 provision run (Joey, dreamstream1) — MinIO side DONE, one recovery step left.
The audit output is the evidence this ticket wanted:
The one failure: the OpenBao write 403'd (
permission deniedonsecret/ci/backend/minio/movement) — theBAO_TOKENused lacked write onsecret/data/ci/backend/minio/*. Since the script never prints secrets and MinIO secret keys are returned exactly once, movement-svc's generated secret is unrecoverable. That's the exact scenariorotate-minio-ci-keys.shdocuments; recovery is one command with an admin-capable token:(the rotate loop is generic — works with the current checkout; infra PR #128 adds
movementto its default set)Then: dispatch/redeploy the file-movement workflow (backend #415 is merged — it now fetches the movement path; until the rotate runs, its guard refuses loudly, which is the designed fail-safe). Close this ticket on: EICAR upload → object in
spiker/quarantine.Board-sweep finding (2026-07-22): HALF-SHIPPED — the movement-svc cutover is fully merged (infra stack Storage__AccessKey=movement-svc + the file-movement workflow's MOVEMENT secret plumbing), but the RUNNING service still carries Storage__AccessKey=metadata-svc — the env change never rolled to the swarm. Dispatched the file-movement workflow just now to redeploy properly (Bao-fetched secret); once the service env shows movement-svc and a quarantine write succeeds, this closes. Will follow up after the rollout.
Audited against
origin/master— all the code and provisioning landed; only the one-time key rotation remains. Staying open on that single step.Committed and complete:
spikersoft-file-movement/docker-stack.yml:32-33—Storage__AccessKey=movement-svc,Storage__SecretKey=${MOVEMENT_S3_SECRET_KEY}. The borrowedmetadata-svckey is gone from the stack, and the header at:18-20documents why (movement-svcgets rw on ebooks/quarantine/dlls/assets/blogs — the bucketsmetadata-svc's policy could not write).openbao/provision-minio-svc-users.sh:20provisionsmovement-svcwith exactly that policy. The note at:47-50records that this resolved the runbook's open decision the way it recommended, and that the stack env swap plus the backend workflow'ssecret/ci/backend/minio/movementpath shipped in the paired PRs.provision-minio-svc-users.sh:60androtate-minio-ci-keys.sh:55honourONLY_SVCS, soONLY_SVCS="movement" ./rotate-minio-ci-keys.shis a real, scoped command rather than an all-or-nothing rotation.Remaining — one action, unrecorded: run
ONLY_SVCS="movement" rotate-minio-ci-keys.shwith an admin-capable Bao token, so thesecret/ci/backend/minio/movementpath actually holds a key. Per comment 10415 the state was HALF-SHIPPED — merged stack env, running service still onmetadata-svc— and no infra commit since then records the rotation. Git can't prove it either way; the settling evidence is a successful File Movementcreate_manifestagainst the quarantine bucket.This is blocking more than it looks. #529's phase-0 items and #783's follow-up both name this rotation as outstanding, and #529's whole 7-service cutover sits behind it. Given the fix is a single scoped command against tooling that already exists, this is probably the highest ratio of unblocked-work to effort currently on the board.