Track the files scaffold; sync deployed oCIS compose requirements

infra/occupymedia/files/ was gitignored in phase one ('nothing deployed');
the oCIS deployment is now live, and the storage doc + DEPLOY.md link to its
runbook. Track the secret-free scaffold (compose.yml, csp.yaml, README,
tuwunel example, .env.example); keep only .env + secrets/ ignored.

compose.yml carries the deployment-proven fixes: ocis runs as user 1000 with
OCIS_CONFIG_DIR=/etc/ocis and HOME=/var/lib/ocis (source-built images do not
inherit the official image's conventions), and 9200/9980 publish on the LAN
for the CT150 tunnel connector.
This commit is contained in:
glass beadian 2026-10-11 07:51:59 +00:00
parent fc5f94400f
commit 6cdffa60b2
6 changed files with 382 additions and 1 deletions

3
.gitignore vendored
View file

@ -27,5 +27,6 @@ docs/proposal.md
docs/work-breakdown.md
docs/frontend-deep-dive.md
docs/s2a-compositor-gate-report.md
infra/occupymedia/files/
infra/occupymedia/files/.env
infra/occupymedia/files/secrets/
apps/media-worker/tmp-wp12/

View file

@ -0,0 +1,37 @@
# Standalone occ-files scaffold. Empty values are intentional deployment gates.
# Keep filled .env private (gitignored). Never copy app/Matrix R2 keys here.
# Pin approved release images by digest; research proposes oCIS 8.2.1.
OCIS_IMAGE=
COLLABORA_IMAGE=
FILES_PUBLIC_URL=https://files.occupymedia.cc
OFFICE_PUBLIC_URL=https://office.occupymedia.cc
FILES_HOST=files.occupymedia.cc
OFFICE_HOST=office.occupymedia.cc
FILES_R2_ENDPOINT=
FILES_R2_BUCKET=occupymedia-files-blobs
FILES_R2_ACCESS_KEY=
FILES_R2_SECRET_KEY=
# Dedicated existing buckets; contracts only, not consumed by this compose.
MATRIX_R2_BUCKET=occupymedia-matrix-media
S3_BUCKET=occupymedia-media-encrypted
PEERTUBE_VIDEO_BUCKET=occupymedia-peertube-videos
PEERTUBE_PLAYLIST_BUCKET=occupymedia-peertube-playlists
FILES_RESERVED_BUCKETS=occupymedia-peertube-videos,occupymedia-peertube-playlists
# Generate/store privately on the target. Same JWT in oCIS and collaboration.
OCIS_JWT_SECRET=
OCIS_ADMIN_PASSWORD=
COLLABORATION_WOPI_SECRET=
COLLABORA_ADMIN_USER=
COLLABORA_ADMIN_PASSWORD=
# Absolute existing regular file outside this repository; required for CODE 26+.
COLLABORA_PROOF_KEY_FILE=
# Reserved for a later implemented bot. Do not start a bot from this scaffold.
HUB_ARCHIVE_HOMESERVER_URL=https://chat.occupymedia.cc
HUB_ARCHIVE_ACCESS_TOKEN=
HUB_ARCHIVE_OCIS_SERVICE_TOKEN=
HUB_ARCHIVE_MAX_BYTES=20971520
HUB_ARCHIVE_CONCURRENCY=1
# Room -> workspace/group/Space/folder mapping comes from an ACL-controlled store.
# Pad uploads/text/revisions must have an R2 adapter before this boundary opens.
PADS_ENABLED=false
PADS_PUBLIC_URL=https://pads.occupymedia.cc

View file

@ -0,0 +1,183 @@
# OccupyMedia files/docs foundation
Status: **scaffolding only; nothing deployed**. This is a new `occ-files`
compose project. Do not merge it into `occ-chat`, `occ-app`, or the historical
one-box compose. Live chat remains conduwuit v0.5.0; this phase does not switch
its image, config, database or volumes. See [storage architecture and contracts](../../../docs/occupymedia-storage.md).
## Payload boundary
R2 is the only durable destination for file/media payloads. oCIS S3NG requires
local POSIX metadata; Matrix requires local state. These are backup-critical
exceptions, not permission to store file bodies, thumbnails, pad revisions or
document caches on Proxmox disks. Transient processing uses bounded RAM/tmpfs.
Disable swap for those workloads (including host/CT swap) and payload-bearing
core dumps. An R2 outage must fail writes, not fall back to disk.
The compose masks the expected upload staging path with tmpfs and omits the
thumbnail service. This is a **candidate configuration**, not proof that every
path in an arbitrary upstream image complies. Audit actual paths and upload
behavior for the pinned releases before admitting any user data. tmpfs upload
sessions disappear on restart and must be retried. Start with a 20 MiB request
cap enforced by the ingress and one upload at a time; large media goes through
the existing dedicated media/video plane.
## Configuration
`.env.example` deliberately leaves images, account endpoint and secrets empty.
Select compatible release digests; do not use `latest` or assume the research's
version availability is sufficient validation. Use the same oCIS image for both
oCIS and collaboration. Confirm S3NG signing/path-style compatibility against
R2 with that release. The native `STORAGE_USERS_S3NG_*` settings explicitly select
the exclusive files bucket. `files-config.ts` is an opt-in validation interface,
not startup wiring; a later adapter must invoke it and enforce the same checks.
Verify the bucket inventory manually before provisioning: `FILES_RESERVED_BUCKETS`
must include every other product's actual bucket names, including both PeerTube
buckets. Never change live app bucket names or its KEK as part of this phase.
Source-built images (Apache-2.0): the repo Dockerfile build runs as root and
defaults its config search to `$HOME/.ocis` — it does NOT inherit the official
image's conventions. The compose therefore pins `user: "1000"`,
`HOME: /var/lib/ocis`, and `OCIS_CONFIG_DIR: /etc/ocis` for the ocis service,
and publishes 9200/9980 on the LAN (no WAN forward exists; the CT150 tunnel
connector reaches occ-files over the bridge). Do not remove these when
swapping images.
Use the built-in oCIS identity provider for an isolated first deployment.
Do not point its issuer at chat: current conduwuit is not the proposed OIDC
provider. External OIDC and account mapping need a separate design/test pass.
For CODE 26+, generate its WOPI proof key privately with the selected image's
upstream procedure, store it outside the repository, and set
`COLLABORA_PROOF_KEY_FILE` to an existing regular file. Keep WOPI proof
verification enabled. The bind mount never creates a directory on a missing
file path. Review required ownership/permissions for the selected images,
including tmpfs/jail and oCIS UID/GID (scaffold assumes 1000).
## Exact next deployment steps (operator work; not executed in phase one)
1. Inventory the existing buckets and CTs. Reserve a **new** files CT/project
with **8 GiB RAM / 4 vCPU** for the complete docs target; keep API and PeerTube
in their current separate CTs. Provide an XFS/ZFS metadata filesystem with
working xattrs, initial 10–20 GiB capacity, monitoring and off-host backups.
Do not reuse existing Matrix volumes. Allocate/verify no swap and no payload
dumps before user uploads.
2. Verify/create `occupymedia-files-blobs` if absent. Keep it private and exclusive
to oCIS. Provision a bucket-scoped read/write key just for files; Matrix and
AnarchoPoster credentials remain separate. Verify lifecycle rules cannot
silently expire active opaque oCIS blobs. Define a separate backup account
and scheduled copy/restore job; durability alone is not a backup.
3. Copy the scaffold to the new CT. Run `cp .env.example .env` and `chmod 600 .env`
in that directory. Fill real private credentials, endpoint and approved
digest-pinned images, public URLs/hosts, independent random admin/JWT/WOPI
secrets and proof-key path. Back up secrets in the password manager. Keep
generated `/etc/ocis/ocis.yaml` and all keys out of git. No secrets are supplied
by this repository.
4. Validate without launching containers:
```bash
cd infra/occupymedia/files
docker compose --env-file .env -f compose.yml --profile docs config --quiet
```
This checks syntax/required variables only. Compare the files bucket against
Matrix, AnarchoPoster and PeerTube names; verify HTTPS account R2 endpoint,
host/URL agreement, image digests and private key-file ownership. Do not print
rendered config or `docker inspect` into shared logs (they contain secrets).
5. On the new project only, initialize once:
```bash
docker compose --env-file .env -f compose.yml run --rm ocis init
docker compose --env-file .env -f compose.yml up -d ocis
```
Protect initialization output (it may contain generated credentials). Do not
use an `init || true` startup loop; inspect unexpected init errors. Verify the
persisted generated config contains no local user-blob driver override.
6. Route `files.occupymedia.cc` through the existing Tunnel to the **new CT's**
`127.0.0.1:9200`, and `office.occupymedia.cc` to `127.0.0.1:9980`. These binds
require a Tunnel connector on that CT, or an explicit authenticated private
relay; the app CT connector cannot reach another CT's loopback. Do not expose
these ports publicly. Preserve HTTPS origin/Host headers, WebSockets, encoded
WOPI URLs and adequate WebDAV/TUS timeouts. Test actual Cloudflare request-size
limits and chunking; Tunnel does not automatically remove those limits.
7. Before opening docs, start the optional pair:
```bash
docker compose --env-file .env -f compose.yml --profile docs up -d collaboration collabora
```
oCIS Web permits only the configured office frame origin via `csp.yaml`.
Collaboration registers with oCIS over private NATS/CS3 and exposes WOPI only
on the compose network. Collabora uses `http://collaboration:9300` as its
allowlisted WOPI source; the browser uses HTTPS office URLs. Verify discovery,
proof keys, locks, read/write grants and CSP. Do not bypass proof checks or
disable TLS verification to make a failed launch appear successful.
8. Admission rehearsal: upload a disposable ODT/image, interrupt a TUS upload,
restart services, retrieve and checksum the saved object, then co-edit an ODT
with two authorized users and reload it after closing both clients. Observe
uploads, jail/temp paths, writable container layers, system storage (including
avatars), and metadata volumes: **no payload may persist outside R2**. Inspect
all release-specific spill/cache paths; mask/disable them or block deployment
if unresolved. Disable avatar/system-file features if they store payloads
locally. Check R2 failure returns an error, revoked users cannot fetch files,
tmpfs exhaustion fails safely, and a backup restores an independent project.
Production is blocked until these checks pass.
9. Create restricted oCIS Spaces and explicit group/room mappings. Implement the
`HubArchivePort` sync-loop adapter + metadata index described in the contract
before enabling mirroring. No bot image, API route or schema migration exists
in this phase. Reserve an unencrypted archive room per group; require explicit
membership/grant reconciliation. Keep encrypted rooms excluded.
10. Rehearse Tuwunel separately with fresh state and disposable media using
`tuwunel-r2.toml.example` merged into the selected release's full config.
Privately supply `endpoint`, `key`, `secret`; the example deliberately lacks
usable credentials. Retain the deployed server name for any eventual live
upgrade, but do not connect the rehearsal to live federation/volumes. A future
authorized migration needs snapshots, compatibility checks, rollback and a
media-copy plan. This phase authorizes none of those live changes.
11. Add a compliant pad adapter only after it can durably save **text, revisions,
attachments and exports** to R2. Leave `PADS_ENABLED=false` until then. An
ordinary local Postgres pad deployment plus periodic R2 exports does not
satisfy this requirement. Add optional Tika only after extraction spill paths
and the policy for storing derived document text have been resolved.
## Resources and recovery
| New files/docs component | Planning RAM / CPU |
|---|---|
| oCIS incl. bounded upload scratch | 0.5–1.5 GiB / 1 vCPU cap |
| WOPI collaboration | 0.1–0.25 GiB / 0.25 vCPU cap |
| Collabora incl. tmpfs jail/temp | 1–2.5 GiB / 1.5 vCPU cap |
| Future hub-archive | 0.1–0.25 GiB / 0.25 vCPU allowance |
| Optional future Tika | 0.5–0.75 GiB / 0.5 vCPU allowance |
| Future pad adapter | 0.2–0.5 GiB / 0.25 vCPU allowance |
Current compose caps total **4.25 GiB / 2.75 vCPU**. Reserve **8 GiB / 4 vCPU**
for the complete docs target and OS/headroom; actual Collabora concurrency must
be measured. A 4 GiB CT is only a reduced files pilot, not this complete stack.
RAM/tmpfs counts toward limits. The 20 MiB/one-transfer initial limit is deliberate;
large originals and multiple active ODT sessions require measurements before
raising it. No pad, bot or Tika RAM is being consumed by this scaffold.
Daily RPO target: consistent oCIS metadata/config snapshots preserving xattrs,
symlinks and ownership **plus** independent R2 object backups; store a manifest
linking the snapshot and object-copy checkpoints. Back up generated identity
and signing configuration and the Collabora proof key on change. Later add Hub
DB index/mapping/outbox/sync-cursor dumps, and Matrix state snapshots via its
supported backup method. Protect the AnarchoPoster KEK separately. Restore-test
monthly in isolated projects; ensure metadata references resolve before reopening
access. R2 alone cannot reconstruct the oCIS directory/ACL tree. Do not back up
tmpfs scratch or document jails to local storage. Retention/deletion actions must
be coordinated across copies, trash/versions and backup expiry, with legal holds.
## Upstream verification
The research is design input, not executable deployment instructions. Settings
were checked against [oCIS storage-users](https://doc.owncloud.com/ocis/8.2/admin/deployment/services/s-list/storage-users.html),
[storage drivers](https://owncloud.dev/ocis/storage/storagedrivers/),
[collaboration](https://doc.owncloud.com/ocis/8.2/admin/deployment/services/s-list/collaboration.html),
[the office compose example](https://github.com/owncloud/ocis/blob/master/deployments/examples/ocis_full/collabora.yml),
and [Tuwunel's config reference](https://github.com/matrix-construct/tuwunel/blob/main/tuwunel-example.toml).
Recheck the exact pinned release before running it; current upstream examples
may differ (notably CODE 26 proof-key initialization). The research's mixed local/R2
Matrix provider sketch is an eventual transition design, not an R2-only end state.

View file

@ -0,0 +1,128 @@
# Phase-one scaffold, NOT deployed. Independent of occ-app/occ-chat/occ-tube.
# See README.md: admission checks are mandatory before accepting user uploads.
name: occ-files
x-runtime: &runtime
restart: unless-stopped
logging:
driver: local
options: { max-size: "10m", max-file: "3" }
networks: [files]
services:
ocis:
<<: *runtime
image: ${OCIS_IMAGE:?set an approved oCIS image digest}
# Initialize privately once with `run --rm ocis init`; fail on absent config.
command: [server]
user: "1000"
environment:
HOME: /var/lib/ocis
OCIS_CONFIG_DIR: /etc/ocis
OCIS_URL: ${FILES_PUBLIC_URL:?}
OCIS_INSECURE: "false"
OCIS_JWT_SECRET: ${OCIS_JWT_SECRET:?}
IDM_ADMIN_PASSWORD: ${OCIS_ADMIN_PASSWORD:?}
IDM_CREATE_DEMO_USERS: "false"
PROXY_TLS: "false" # host-side Tunnel terminates public TLS
PROXY_HTTP_ADDR: 0.0.0.0:9200
PROXY_ENABLE_BASIC_AUTH: "false"
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/ocis/csp.yaml
OFFICE_HOST: ${OFFICE_HOST:?}
STORAGE_USERS_DRIVER: s3ng
STORAGE_USERS_S3NG_ROOT: /var/lib/ocis/storage/users
STORAGE_USERS_S3NG_ENDPOINT: ${FILES_R2_ENDPOINT:?set the HTTPS account R2 endpoint}
STORAGE_USERS_S3NG_REGION: auto
STORAGE_USERS_S3NG_BUCKET: ${FILES_R2_BUCKET:?dedicated files bucket only}
STORAGE_USERS_S3NG_ACCESS_KEY: ${FILES_R2_ACCESS_KEY:?bucket-scoped files key}
STORAGE_USERS_S3NG_SECRET_KEY: ${FILES_R2_SECRET_KEY:?}
OCIS_MAX_CONCURRENCY: "1"
OCIS_ASYNC_UPLOADS: "false"
# No previews/content extraction in phase one: they are payload derivatives.
OCIS_EXCLUDE_RUN_SERVICES: thumbnails
THUMBNAILS_FILESYSTEMSTORAGE_ROOT: /tmp/thumbnails
GATEWAY_GRPC_ADDR: 0.0.0.0:9142
MICRO_REGISTRY_ADDRESS: 127.0.0.1:9233
NATS_NATS_HOST: 0.0.0.0
NATS_NATS_PORT: "9233"
ports: ["9200:9200"]
volumes:
- ocis-config:/etc/ocis
- ocis-metadata:/var/lib/ocis
- ./csp.yaml:/etc/ocis/csp.yaml:ro
# Defense against upload staging spilling into the metadata volume.
# Verify actual paths for the chosen release, including partial TUS uploads.
tmpfs:
- /tmp:size=268435456,mode=1777
- /var/lib/ocis/storage/users/uploads:size=268435456,uid=1000,gid=1000,mode=0700
mem_limit: 1536m
cpus: 1.0
collaboration:
<<: *runtime
profiles: [docs]
image: ${OCIS_IMAGE:?}
command: [collaboration, server]
depends_on:
- ocis
- collabora
environment:
OCIS_URL: ${FILES_PUBLIC_URL:?}
OCIS_JWT_SECRET: ${OCIS_JWT_SECRET:?}
# Must be the registry service name (looked up via MICRO_REGISTRY), NOT host:port.
# Reva pool.GetService() -> 'com.owncloud.api.gateway'; host:port values are only
# honored with an explicit dns:/unix:/kubernetes: URI scheme prefix.
OCIS_REVA_GATEWAY: com.owncloud.api.gateway
MICRO_REGISTRY: nats-js-kv
MICRO_REGISTRY_ADDRESS: ocis:9233
COLLABORATION_STORE_NODES: ocis:9233
COLLABORATION_GRPC_ADDR: 0.0.0.0:9301
COLLABORATION_HTTP_ADDR: 0.0.0.0:9300
COLLABORATION_WOPI_SRC: http://collaboration:9300
COLLABORATION_WOPI_SECRET: ${COLLABORATION_WOPI_SECRET:?}
COLLABORATION_APP_NAME: CollaboraOnline
COLLABORATION_APP_PRODUCT: Collabora
COLLABORATION_APP_ADDR: ${OFFICE_PUBLIC_URL:?}
COLLABORATION_APP_INSECURE: "false"
COLLABORATION_APP_PROOF_DISABLE: "false"
COLLABORATION_CS3API_DATAGATEWAY_INSECURE: "false"
volumes: [ocis-config:/etc/ocis:ro]
tmpfs: ["/tmp:size=67108864,mode=1777"]
mem_limit: 256m
cpus: 0.25
collabora:
<<: *runtime
profiles: [docs]
image: ${COLLABORA_IMAGE:?set an approved Collabora image digest}
environment:
DONT_GEN_SSL_CERT: "YES"
aliasgroup1: http://collaboration:9300
username: ${COLLABORA_ADMIN_USER:?}
password: ${COLLABORA_ADMIN_PASSWORD:?}
extra_params: >-
--o:ssl.enable=false
--o:ssl.termination=true
--o:net.frame_ancestors=${FILES_HOST:?}
--o:welcome.enable=false
cap_add: [MKNOD]
ports: ["9980:9980"]
volumes:
- type: bind
source: ${COLLABORA_PROOF_KEY_FILE:?private existing proof key file}
target: /etc/coolwsd/proof_key
read_only: true
bind: { create_host_path: false }
# No persistent document volume; jail/temp writes consume RAM, counted in limit.
tmpfs:
- /tmp:size=268435456,mode=1777
- /opt/cool/child-roots:size=1073741824,mode=1777
mem_limit: 2560m
cpus: 1.5
# ONLY config + POSIX metadata/state. Audit their contents before opening uploads.
volumes:
ocis-config:
ocis-metadata:
networks:
files:

View file

@ -0,0 +1,14 @@
# Minimal ownCloud Web CSP with the office frame origin; oCIS expands ${VAR}.
directives:
default-src: ["'none'"]
child-src: ["'self'"]
connect-src: ["'self'", "blob:"]
font-src: ["'self'", "data:"]
frame-ancestors: ["'self'"]
frame-src: ["'self'", "blob:", "https://${OFFICE_HOST}"]
img-src: ["'self'", "data:", "blob:", "https://${OFFICE_HOST}"]
manifest-src: ["'self'"]
media-src: ["'self'"]
object-src: ["'self'", "blob:"]
script-src: ["'self'", "'unsafe-inline'"]
style-src: ["'self'", "'unsafe-inline'"]

View file

@ -0,0 +1,18 @@
# FUTURE config fragment for an isolated Tuwunel rehearsal, not conduwuit v0.5.0.
# Do not mount this in occ-chat or substitute images/volumes during phase one.
# Merge into the selected release's full config privately; TOML does NOT expand env.
# This represents the fresh R2-only target, not a migration strategy.
[global]
media_storage_providers = ["r2"]
store_media_on_providers = ["r2"]
media_allow_redirect = true
error_on_unknown_config_opts = true
[global.storage_provider.r2.s3]
bucket = "occupymedia-matrix-media"
region = "auto"
use_vhost_request = false
startup_check = true
# Privately supply endpoint, key, secret with the selected version's secret mechanism.
# Endpoint: https://<account-id>.r2.cloudflarestorage.com
# Never reuse the oCIS/AnarchoPoster/PeerTube bucket or key.