Ansible playbooks to deploy ownCloud Infinite Scale (oCIS) on a hardened Debian 13 VM on Proxmox VE.
Why oCIS over ownCloud 10? oCIS is a ground-up Go rewrite: single binary, no PHP, no database, no Redis — dramatically fewer moving parts. Its decomposedfs storage replaces the Nextcloud/ownCloud 10 POSIX+DB model. Built-in IDP, JWT-based microservice mesh, and a first-class REST/Graph API make it the forward path for ownCloud deployments. Memory footprint for a small team is roughly 300–500 MB idle, well below a comparable ownCloud 10 LAMP stack.
Internet
│ 80/443
▼
┌──────────────────────────────────────────────────┐
│ edge (rootful system Quadlet, runsc/gVisor) │
│ Caddy 2 + xcaddy-coraza WAF (OWASP CRS v4) │
│ TLS termination (acme | byo | selfsigned) │
└──────────────────┬───────────────────────────────┘
│ server-TLS (internal CA, CN=ocis)
│ https://host.containers.internal:9200
▼
┌──────────────────────────────────────────────────┐
│ ocis (rootless user Quadlet, ocisstack uid 1500)│
│ owncloud/ocis:7.1.0, network ocis-internal │
│ proxy :9200 ← HTTPS (decomposedfs on disk) │
│ debug :9205 ← Prometheus scrape target │
│ IDM :9235 ← built-in LDAP │
│ IDP :9130 ← built-in OIDC (default) │
└──────────────────────────────────────────────────┘
│ clamd.sock (shared volume, when clamav_enabled)
▼
┌──────────────────────────────────────────────────┐
│ ocis-clamav (rootless, clamav/clamav image) │
└──────────────────────────────────────────────────┘
collaboration (rootless, collabora/code image) ← when collabora_enabled
node_exporter + prometheus + grafana ← when monitoring_enabled
restic backup timer ← when ocis_backup_enabled
fail2ban (system) ← when fail2ban_enabled
Edge layer: A single rootful Caddy container runs under the runsc gVisor
OCI runtime (systrap platform) with the Coraza WAF module and OWASP Core Rule
Set v4. It terminates public TLS on 80/443 and reverse-proxies to the oCIS
proxy using server-TLS verified against the internal CA (the oCIS server
cert has CN/SAN ocis). This is one-way TLS — oCIS proxy cannot verify client
certificates — not mutual TLS. A mTLS sidecar is a documented follow-up.
App layer: A single rootless owncloud/ocis container runs in user scope
under the lingering ocisstack (UID 1500) account as a Podman Quadlet systemd
user unit. All secrets are injected as environment variables via podman secrets
(not a templated ocis.yaml). The container is attached to the ocis-internal
Podman network.
Secrets model: Ansible generates no secrets of its own. The operator runs
scripts/gen-ocis-secrets.sh once, pastes the output into
inventory/group_vars/all/vault.yml, and encrypts with ansible-vault. At
deploy time, the ocis role creates podman secrets from those vault values and
passes them as OCIS_* / IDM_* environment variables into the container.
| Role | Purpose |
|---|---|
os_base |
Locale, timezone, SSH hardening, firewall contract vars |
podman_host |
Podman, netavark/aardvark-dns, gVisor runsc, ocis-internal network, ocisstack linger |
internal_pki |
Self-signed CA + server cert (CN ocis) for the edge→oCIS hop |
caddy_edge |
Build xcaddy+coraza image; rootful edge container (runsc) |
ocis |
Vault secrets → podman secrets; oCIS container Quadlet; env-var config |
collabora |
Collabora CODE + WOPI collaboration container (gated) |
clamav |
ClamAV + clamd socket shared into oCIS antivirus service (gated) |
fail2ban |
fail2ban jail reading oCIS proxy log (gated) |
monitoring |
node_exporter + Prometheus (scrapes :9205) + Grafana (gated) |
ocis_backup |
restic backup systemd timer for ocis-data + ocis-config volumes (gated) |
proxmox_vm |
Clone Proxmox cloud-init template → new VM (gated by provision_vm_enabled) |
- Proxmox VE with a Debian 13 cloud-init template (built by the sibling
proxmox-debian13-templaterepo, VMID9999). Setpve_template_vmidininventory/group_vars/all/main.ymlto match. - Deploy SSH keypair at
files/ssh/ocis-deploy(private, git-ignored) andfiles/ssh/ocis-deploy.pub(committed). Generate with:ssh-keygen -t ed25519 -f files/ssh/ocis-deploy
- direnv (
direnv allow) or manually:Theexport ANSIBLE_CONFIG=$PWD/ansible.cfg unset ANSIBLE_ROLES_PATH unset ANSIBLE_COLLECTIONS_PATH unset ANSIBLE_COLLECTIONS_PATHS
.envrcunsets those three variables. If they are set in your shell profile they override the projectansible.cfgand break collection/role resolution. If you run ansible-lint or ansible-playbook without direnv, use the explicit unset form:env -u ANSIBLE_COLLECTIONS_PATHS -u ANSIBLE_COLLECTIONS_PATH ansible-lint ANSIBLE_ROLES_PATH=roles ansible-playbook --syntax-check site.yml
- Collections:
ansible-galaxy collection install -r requirements.yml
- VM sizing: oCIS is a single Go binary — 2 vCPU / 4 GB RAM is sufficient for a small team without ClamAV. Enable ClamAV or the local monitoring dashboard and budget an extra 1–2 GB. The Vagrantfile reflects these minimums.
- Run the generator (requires
opensslanduuidgen):scripts/gen-ocis-secrets.sh
- Paste the entire output into
inventory/group_vars/all/vault.yml, replacing everychange-me-*placeholder. - Encrypt:
ansible-vault encrypt inventory/group_vars/all/vault.yml
- Every subsequent
ansible-playbookrun requires--ask-vault-pass(or--vault-password-file).
The ocis role translates the vault keys into podman secrets and wires them as
Secret= entries in the Quadlet .container unit. The container never reads a
file-based ocis.yaml; all configuration is environment-variable-driven.
| Vault key | Podman secret / env var |
|---|---|
vault_ocis_jwt_secret |
ocis-jwt-secret / OCIS_JWT_SECRET |
vault_ocis_transfer_secret |
ocis-transfer-secret / OCIS_TRANSFER_SECRET |
vault_ocis_machine_auth_api_key |
ocis-machine-auth-api-key / OCIS_MACHINE_AUTH_API_KEY |
vault_ocis_system_user_api_key |
ocis-system-user-api-key / OCIS_SYSTEM_USER_API_KEY |
vault_ocis_storage_system_jwt_secret |
ocis-storage-system-jwt-secret / STORAGE_SYSTEM_JWT_SECRET |
vault_ocis_service_account_secret |
ocis-service-account-secret / OCIS_SERVICE_ACCOUNT_SECRET |
vault_ocis_admin_password |
ocis-idm-admin-password / IDM_ADMIN_PASSWORD |
vault_ocis_idm_svc_password |
ocis-idm-svc-password / IDM_SVC_PASSWORD |
vault_ocis_idm_revasvc_password |
ocis-idm-revasvc-password / IDM_REVASVC_PASSWORD |
vault_ocis_idm_idpsvc_password |
ocis-idm-idpsvc-password / IDM_IDPSVC_PASSWORD |
vault_ocis_collaboration_wopi_secret |
ocis-collaboration-wopi-secret / COLLABORATION_WOPI_SECRET |
vault_ocis_system_user_id |
injected as OCIS_SYSTEM_USER_ID |
vault_ocis_service_account_id |
injected as OCIS_SERVICE_ACCOUNT_ID |
vault_ocis_backup_password |
used by the ocis_backup restic role |
By default oCIS runs with its built-in IDP (Keycloak-compatible OIDC issuer). No extra infrastructure is needed.
To switch to an external OIDC provider (e.g. Keycloak), set:
# inventory/group_vars/all/main.yml
ocis_oidc_external_enabled: true
ocis_oidc_issuer: "https://keycloak.example.net/realms/ocis"
ocis_oidc_web_client_id: webWhen ocis_oidc_external_enabled: true, the ocis role appends idp to
OCIS_EXCLUDE_RUN_SERVICES, removing the bundled IDP from the run set. The
OCIS_OIDC_ISSUER environment variable is set to ocis_oidc_issuer. Your
Keycloak realm must pre-exist; the role does not manage it.
All integrations are gated by a boolean variable. Disable any by setting the
corresponding flag to false in inventory/group_vars/all/main.yml.
| Integration | Variable | What it does |
|---|---|---|
| Collabora Online | collabora_enabled |
Deploys a second rootless collabora/code container + oCIS WOPI collaboration sidecar. Registers as the WOPI app in oCIS. |
| ClamAV antivirus | clamav_enabled |
Deploys clamav/clamav; shares a clamd.sock unix socket volume into oCIS. Activates antivirus in OCIS_ADD_RUN_SERVICES. |
| Monitoring | monitoring_enabled |
node_exporter + Prometheus (scrapes oCIS debug endpoint :9205) + optional on-box Grafana (monitoring_local_dashboard). |
| fail2ban | fail2ban_enabled |
Jail reading the oCIS proxy log for brute-force attempts. |
| Restic backups | ocis_backup_enabled |
Nightly restic snapshot of ocis-data and ocis-config volumes at 02:30. |
| Proxmox VM create | provision_vm_enabled |
Clone the Proxmox template into a new VM before provisioning. Off by default. |
All top-level variables live in inventory/group_vars/all/main.yml; role
tunables live in roles/<role>/defaults/main.yml.
| Variable | Default | Notes |
|---|---|---|
ocis_domain |
cloud.example.net |
Public FQDN; used in certs, edge vhost, oCIS trusted domains |
collabora_domain |
office.example.net |
Collabora public FQDN |
grafana_domain |
grafana.example.net |
Grafana dashboard FQDN |
ocis_version |
7.1.0 |
oCIS image tag — pin deliberately, bump with care |
tls_mode |
selfsigned |
acme (Let's Encrypt), byo (vault cert/key), selfsigned (lab) |
edge_tls_profile |
intermediate |
intermediate (TLS 1.2+) or modern (TLS 1.3 only) |
waf_mode |
enforce |
Coraza/CRS mode — soak in detection before switching to enforce |
gvisor_platform |
systrap |
Never kvm — no nested virt on a cloned VM |
ocis_oidc_external_enabled |
false |
Switch to external OIDC; excludes bundled idp service |
collabora_enabled |
true |
Deploy Collabora Online |
clamav_enabled |
true |
Deploy ClamAV antivirus |
fail2ban_enabled |
true |
Deploy fail2ban |
monitoring_enabled |
true |
Deploy node_exporter + Prometheus |
monitoring_local_dashboard |
false |
On-box Grafana |
ocis_backup_enabled |
true |
Restic backups |
provision_vm_enabled |
false |
Clone Proxmox template |
pve_vm_cores |
2 |
VM vCPUs |
pve_vm_memory |
4096 |
VM RAM in MB |
management_cidrs |
[] |
CIDRs allowed SSH (22) |
firewall_allow_tcp |
[80, 443] |
Public TCP ports |
Full OS hardening is owned by the sibling
ansible-hardening-debian
repo and runs as a separate orchestration layer. Run in this order:
provision-vm.yml(or use an existing Debian 13 host).ansible-hardening-debian:bootstrap.ymlthen itssite.yml.- This repo's
site.yml, connecting as the hardening repo's deploy user.
CRITICAL: Rootless Podman and gVisor systrap both require unprivileged user namespaces. An over-aggressive hardening profile that sets
kernel.unprivileged_userns_clone=0will break the entire stack. Confirmkernel.unprivileged_userns_clone=1anduser.max_user_namespaces>0before and after hardening.
Rootless systemd-user linger, gVisor systrap, and rootless Collabora all require a real VM with unprivileged user namespaces enabled.
vagrant up # boots the VM and runs site.yml via the Ansible provisioner
vagrant provision # re-run the playbook after changesvagrant up is self-contained: before provisioning it auto-generates an
unencrypted dev vault at inventory/group_vars/all/vault.yml (git-ignored)
from scripts/gen-ocis-secrets.sh, so no vault password is needed for the local
box. Delete that file to regenerate fresh secrets. This dev convenience is
Vagrant-only — for remote/production runs you create and ansible-vault encrypt
the vault yourself (below) and pass --ask-vault-pass.
ansible-playbook -i inventory/test.yml site.yml --ask-vault-passansible-playbook -i inventory/hosts.yml site.yml \
-e provision_vm_enabled=true --ask-vault-passansible-playbook -i inventory/vagrant.yml tests/test-ocis.yml --ask-vault-passRun all tests in dependency order:
for t in os_base podman_host internal_pki caddy_edge \
ocis collabora clamav fail2ban monitoring ocis_backup; do
ansible-playbook -i inventory/vagrant.yml tests/test-$t.yml --ask-vault-pass || break
done- Internal mTLS sidecar: The edge currently uses one-way server-TLS to reach oCIS (the edge verifies the internal CA cert; oCIS proxy cannot verify client certs). A mTLS sidecar that terminates client auth before handing off to oCIS is documented but not yet implemented.
- clamd cross-container socket verification: The clamd unix socket is shared
via a Podman volume from the
ocis-clamavcontainer into theociscontainer. Live round-trip verification under concurrent upload load has not been run yet. - Collabora WOPI nats reachability: The
collaborationservice registers with oCIS via a nats endpoint (:9233). End-to-end collaborative editing under theocis-internalPodman network has not been verified in a live lab. - Vagrant end-to-end smoke test:
vagrant up+tests/test-ocis.ymlwas not run (no KVM available in the dev environment). Verify manually before production use.