diff --git a/.gitignore b/.gitignore index 3df549e..14f0965 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/infra/occupymedia/files/.env.example b/infra/occupymedia/files/.env.example new file mode 100644 index 0000000..58c5202 --- /dev/null +++ b/infra/occupymedia/files/.env.example @@ -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 diff --git a/infra/occupymedia/files/README.md b/infra/occupymedia/files/README.md new file mode 100644 index 0000000..78235bd --- /dev/null +++ b/infra/occupymedia/files/README.md @@ -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. diff --git a/infra/occupymedia/files/compose.yml b/infra/occupymedia/files/compose.yml new file mode 100644 index 0000000..6676a44 --- /dev/null +++ b/infra/occupymedia/files/compose.yml @@ -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: diff --git a/infra/occupymedia/files/csp.yaml b/infra/occupymedia/files/csp.yaml new file mode 100644 index 0000000..d6124a6 --- /dev/null +++ b/infra/occupymedia/files/csp.yaml @@ -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'"] diff --git a/infra/occupymedia/files/tuwunel-r2.toml.example b/infra/occupymedia/files/tuwunel-r2.toml.example new file mode 100644 index 0000000..dfd9592 --- /dev/null +++ b/infra/occupymedia/files/tuwunel-r2.toml.example @@ -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://.r2.cloudflarestorage.com +# Never reuse the oCIS/AnarchoPoster/PeerTube bucket or key.