Skip to main content

Phase 57-58 β€” Nextcloud 33 + Authentik SSO + OnlyOffice

Phase 57 (2026-06-22): Nextcloud 33.0.5 on k3s with Authentik OIDC SSO. Phase 58 (2026-07-14): OnlyOffice DocumentServer 8.3.3 added β€” in-browser editing for .docx/.xlsx/.pptx files directly from the Nextcloud Files UI.


What Was Deployed​

ComponentVersionNotes
Nextcloud33.0.5Helm chart via ArgoCD
user_oidc plugin8.10.1Bundled in the chart
PostgreSQLBitnami (Longhorn PVC)Dedicated DB for Nextcloud
RedisBitnamiSession cache + file-locking
Authentik providerpk=9OIDC, explicit-consent flow
OnlyOffice DocumentServer8.3.3Custom image: minicloud-onlyoffice
onlyoffice Nextcloud app10.1.2Nextcloud ↔ OnlyOffice bridge

URLs:

AccessURL
Nextcloud internalhttps://cloud.10.0.0.200.nip.io
Nextcloud publichttps://cloud.devandre.sbs
OnlyOffice internalhttps://onlyoffice.10.0.0.200.nip.io
OnlyOffice publichttps://onlyoffice.devandre.sbs

Admin password: ssh controller 'cat ~/.nextcloud-admin'


Architecture​

Browser β†’ Cloudflare Tunnel β†’ NGINX Ingress (10.0.0.200)
β”‚
β”œβ”€β”€ / β†’ nextcloud pod (nextcloud namespace)
β”‚ β”‚
β”‚ β”œβ”€β”€ PostgreSQL (PVC: longhorn, 10Gi)
β”‚ β”œβ”€β”€ Redis (session + file-lock)
β”‚ β”œβ”€β”€ Authentik (OIDC) ← SSO login
β”‚ └── onlyoffice app (occ configured)
β”‚
└── / β†’ onlyoffice pod (ClusterIP)
└── DocumentServer 8.3.3

Document edit flow:
Browser β†’ opens doc in Nextcloud β†’ Nextcloud redirects iframe to OnlyOffice
OnlyOffice fetches doc from StorageUrl (https://cloud.devandre.sbs/)
OnlyOffice saves changes back β†’ Nextcloud
JWT token authenticates all OnlyOffice ↔ Nextcloud calls

CoreDNS maps cloud.devandre.sbs β†’ 10.0.0.200 in-cluster so Nextcloud's OIDC callback hits NGINX directly without leaving the cluster.


Authentik OIDC Provider Setup​

Nextcloud uses the user_oidc plugin. The Authentik provider must be created via the API β€” the UI omits several required fields.

Create the OAuth2 provider​

Key fields that must be set explicitly (API defaults are wrong):

payload = {
"name": "Nextcloud",
"authorization_flow": <explicit-consent-flow-uuid>,
"invalidation_flow": <default-provider-invalidation-flow-uuid>,
"client_type": "confidential",
"client_id": "nextcloud",
"redirect_uris": [
{
"matching_mode": "strict",
"url": "https://cloud.devandre.sbs/apps/user_oidc/code"
},
{
"matching_mode": "strict",
"url": "https://cloud.10.0.0.200.nip.io/apps/user_oidc/code"
}
],
"grant_types": ["authorization_code", "refresh_token"], # ← MUST set; API default is []
"sub_mode": "hashed_user_id",
"issuer_mode": "per_provider", # ← affects OIDC discovery URL
"include_claims_in_id_token": True,
"signing_key": <rs256-key-uuid>
}

grant_types must be set explicitly. The Authentik API defaults to an empty list [], which means no grant type is allowed and every login attempt fails silently with a 400.

issuer_mode: per_provider gives each Authentik provider its own discovery URL (/application/o/<slug>/). If you later change issuer_mode on an existing provider, clear the Nextcloud Redis cache β€” Nextcloud caches the OIDC discovery document and will keep hitting the old issuer URL:

ssh controller "kubectl exec -n nextcloud deploy/nextcloud -- php occ cache:flush"
# or exec into Redis and FLUSHDB

Allow SSRF for internal OIDC callback​

Nextcloud blocks HTTP requests to private/internal IP ranges by default (SSRF protection). The Authentik provider URL resolves to 10.0.0.200 inside the cluster β€” add it to the allow list:

kubectl exec -n nextcloud deploy/nextcloud -- \
php occ config:system:set allow_local_remote_servers --value=true --type=boolean

Without this, Nextcloud silently fails to fetch the OIDC discovery document.

Do not use default-provider-authorization-implicit-consent. Create an explicit-consent flow in Authentik (default-provider-authorization-explicit-consent) β€” Nextcloud requires user consent to be confirmed before issuing tokens.


Helm Values (key excerpts)​

# minicloud-ansible/helm-values/nextcloud-values.yaml
nextcloud:
host: cloud.10.0.0.200.nip.io
existingSecret:
enabled: true
secretName: nextcloud-secrets
usernameKey: nextcloud-username
passwordKey: nextcloud-password

ingress:
enabled: true
ingressClassName: nginx
hosts:
- host: cloud.10.0.0.200.nip.io
paths: [{path: /, pathType: Prefix}]
- host: cloud.devandre.sbs
paths: [{path: /, pathType: Prefix}]
tls:
- hosts: [cloud.10.0.0.200.nip.io, cloud.devandre.sbs]
secretName: nextcloud-tls

postgresql:
enabled: true
primary:
persistence:
storageClass: longhorn
size: 10Gi

redis:
enabled: true

persistence:
enabled: true
storageClass: longhorn
size: 20Gi

Configuring user_oidc in Nextcloud​

After the Authentik provider is created, register it in Nextcloud via CLI:

kubectl exec -n nextcloud deploy/nextcloud -- \
php occ user_oidc:provider Authentik \
--clientid="nextcloud" \
--clientsecret="<client-secret>" \
--discoveryuri="https://auth.10.0.0.200.nip.io/application/o/nextcloud/.well-known/openid-configuration" \
--mapping-uid="preferred_username" \
--unique-uid=0

Use the nip.io discovery URI (not devandre.sbs) β€” in-cluster DNS maps it to 10.0.0.200 and avoids the CoreDNS β†’ Cloudflare β†’ back round-trip.



OnlyOffice DocumentServer (Phase 58)​

OnlyOffice 8.3.3 is deployed as a separate pod in the nextcloud namespace alongside the Nextcloud Helm release. All manifests live in minicloud-gitops/manifests/nextcloud/ and are tracked via a third source in the ArgoCD Application.

GitOps Multi-Source Layout​

# apps/nextcloud.yaml β€” three sources:
sources:
- repoURL: https://nextcloud.github.io/helm/ # Helm chart
chart: nextcloud
targetRevision: 9.1.3
helm:
valueFiles:
- $values/helm-values/nextcloud-values.yaml
- repoURL: https://github.com/andrelair-platform/minicloud-gitops.git
targetRevision: main
ref: values # $values reference
- repoURL: https://github.com/andrelair-platform/minicloud-gitops.git
targetRevision: main
path: manifests/nextcloud # raw YAML directory

The manifests/nextcloud/ directory contains:

FilePurpose
10-onlyoffice.yamlDeployment + ClusterIP Service
11-onlyoffice-ingress.yamlcert-manager Certificate + NGINX Ingress
12-onlyoffice-rbac.yamlServiceAccount + Role + RoleBinding for config Job
13-onlyoffice-config-job.yamlArgoCD PostSync hook Job (idempotent occ config)

Secret Management​

The JWT shared secret lives in Vault at secret/platform/nextcloud-secrets under key onlyoffice-jwt-secret. ESO fetches the entire secret path via dataFrom.extract β€” all keys appear as fields in the nextcloud-secrets k8s Secret automatically.

# Add the key to Vault (vault CLI not installed on controller β€” use curl):
TOKEN=$(cat ~/.vault-root-token)
ssh controller "curl -sk -X POST \
-H 'X-Vault-Token: $TOKEN' \
-d '{\"data\":{\"onlyoffice-jwt-secret\":\"<64-char-hex>\"}}' \
https://vault.10.0.0.200.nip.io/v1/secret/data/platform/nextcloud-secrets"

# Force ESO to re-sync after adding the key:
kubectl annotate externalsecret nextcloud-secrets -n nextcloud \
force-sync=$(date +%s) --overwrite

Custom Image β€” minicloud-onlyoffice​

The stock onlyoffice/documentserver image does not trust the minicloud self-signed CA. Node.js (which runs ds:docservice and ds:converter inside the container) maintains its own certificate bundle and ignores the OS trust store. Without the CA, HTTPS requests to cloud.devandre.sbs fail TLS verification and the editor shows "Download failed" when trying to fetch documents.

The fix is a custom image that extends the upstream and bakes in the CA:

FROM onlyoffice/documentserver:8.3.3

USER root

COPY certs/minicloud-ca.crt /usr/local/share/ca-certificates/minicloud-ca.crt
RUN update-ca-certificates

# Node.js ignores the OS trust store β€” set explicitly for ds:docservice + ds:converter
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/minicloud-ca.crt

Source: andrelair-platform/minicloud-onlyoffice
CI pushes to: harbor.10.0.0.200.nip.io/library/onlyoffice:<sha>-amd64
GitOps update: CI bumps the image tag in manifests/nextcloud/10-onlyoffice.yaml on every push to main.

OnlyOffice Deployment Specifics​

image: harbor.10.0.0.200.nip.io/library/onlyoffice:<sha>-amd64 # managed by CI
env:
- JWT_ENABLED: "true"
- JWT_HEADER: "AuthorizationJwt" # non-standard header avoids conflicts
- JWT_SECRET: <from nextcloud-secrets>
- ALLOW_PRIVATE_IP_ADDRESS: "true" # OnlyOffice must reach Nextcloud on 10.0.0.x
- ALLOW_META_IP_ADDRESS: "true"
resources:
requests: {cpu: 250m, memory: 1Gi}
limits: {cpu: "2", memory: 3Gi}

Liveness and readiness probes hit /healthcheck on port 80 (initialDelaySeconds: 60 β€” DocumentServer takes 45-60s to start).

The nextcloud namespace is excluded from the Gatekeeper registry allowlist, so docker.io/onlyoffice/documentserver is allowed without a policy exception.

Nextcloud occ Configuration​

The PostSync Job runs these commands after every ArgoCD sync (idempotent):

NC_POD=$(kubectl get pod -n nextcloud -l app.kubernetes.io/name=nextcloud \
-o jsonpath='{.items[0].metadata.name}')

# Install/enable the bridge app
kubectl exec -n nextcloud "$NC_POD" -- php /var/www/html/occ app:install onlyoffice \
|| kubectl exec -n nextcloud "$NC_POD" -- php /var/www/html/occ app:enable onlyoffice

# Browser URL (Cloudflare Tunnel β€” what the user's browser reaches)
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice DocumentServerUrl \
--value="https://onlyoffice.devandre.sbs/"

# Internal URL (cluster DNS β€” what Nextcloud server uses for health checks)
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice DocumentServerInternalUrl \
--value="http://onlyoffice.nextcloud.svc.cluster.local/"

# Storage URL (OnlyOffice fetches/saves docs back to Nextcloud via this URL)
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice StorageUrl \
--value="https://cloud.devandre.sbs/"

# JWT (must match JWT_SECRET env var on the OnlyOffice pod)
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice jwt_secret --value="$JWT_SECRET"
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice jwt_header --value="AuthorizationJwt"

# Default to modern open formats
kubectl exec -n nextcloud "$NC_POD" -- \
php /var/www/html/occ config:app:set onlyoffice defFormats \
--value='{"docx":"1","xlsx":"1","pptx":"1","odt":"","ods":"","odp":""}'

Cloudflare Tunnel Route​

# Run on controller β€” registers onlyoffice.devandre.sbs in Cloudflare DNS:
~/.local/bin/cloudflared tunnel route dns minicloud onlyoffice.devandre.sbs

# Add to ~/.cloudflared/config.yml ingress rules:
- hostname: onlyoffice.devandre.sbs
service: https://10.0.0.200
originRequest:
originServerName: onlyoffice.10.0.0.200.nip.io

# Restart cloudflared:
sudo systemctl restart cloudflared

The originServerName must match the TLS certificate on the NGINX Ingress (onlyoffice.10.0.0.200.nip.io) β€” without it, cloudflared cannot verify the backend TLS certificate and fails with a TLS SNI error.


Gotchas​

IssueSymptomFix
grant_types: [] API defaultLogin fails with 400Set grant_types: ["authorization_code", "refresh_token"] explicitly
SSRF protectionDiscovery fetch silently failsocc config:system:set allow_local_remote_servers --value=true
issuer_mode change cachedOIDC discovery URL mismatch after provider editocc cache:flush and Redis FLUSHDB
Explicit consent requiredToken not issuedUse default-provider-authorization-explicit-consent flow
Cloudflare Tunnel TLS SNIoriginServerName must match nip.io certSet originServerName: cloud.10.0.0.200.nip.io in cloudflared config
ESO key not in secretonlyoffice pod CreateContainerConfigErrorkubectl annotate externalsecret ... force-sync=$(date +%s) --overwrite
bitnami/kubectl no version tagsConfig Job ImagePullBackOffbitnami/kubectl only publishes :latest on Docker Hub β€” use alpine/k8s:1.36.1 instead
Gatekeeper block-latest-tagJob pod blocked by admission webhookNever use :latest; alpine/k8s has explicit version tags matching the cluster
OnlyOffice slow startReadiness probe fails for 45-60sinitialDelaySeconds: 60 on probes is required
JWT header conflictOnlyOffice rejects requests with 401Use JWT_HEADER: AuthorizationJwt (not Authorization) to avoid collision with Nginx auth headers
nginx.org/proxy-buffer-size: 16kNGINX reload fails: proxy_busy_buffers_size errorRemove this annotation β€” with default proxy_buffers, 16k buffer exceeds the limit; NGINX never loads the TLS cert
tls: unrecognized name from cloudflared502 on public URL even though nip.io worksNGINX reload failure (above) means the Ingress TLS block was never applied β€” fix the annotation first
"Download failed" in editorOnlyOffice can't fetch the documentStock image doesn't trust minicloud-ca; use the minicloud-onlyoffice custom image with NODE_EXTRA_CA_CERTS set
devandre.sbs hostname not in Ingress502 from CloudflareBoth nip.io and devandre.sbs hostnames must be in Ingress rules and tls; cert SAN must cover both

Verify SSO​

  1. Navigate to https://cloud.devandre.sbs
  2. Click Login with Authentik
  3. Authentik consent screen appears β†’ Accept
  4. Redirected back to Nextcloud, logged in as Authentik user
  5. User auto-provisioned in Nextcloud β€” no manual user creation needed
# Confirm user was provisioned
kubectl exec -n nextcloud deploy/nextcloud -- php occ user:list

Operational Notes​

# Nextcloud admin CLI (all occ commands run inside the pod)
kubectl exec -n nextcloud deploy/nextcloud -- php occ <command>

# List OIDC providers
kubectl exec -n nextcloud deploy/nextcloud -- php occ user_oidc:provider

# Flush all caches
kubectl exec -n nextcloud deploy/nextcloud -- php occ cache:flush

# Check Nextcloud background jobs
kubectl exec -n nextcloud deploy/nextcloud -- php occ background:cron

# Admin password
ssh controller "cat ~/.nextcloud-admin"
# Verify OnlyOffice health check
/usr/bin/curl --cacert ~/minicloud-ca.crt -sI https://onlyoffice.10.0.0.200.nip.io/healthcheck

# Check OnlyOffice pod logs
kubectl --context minicloud logs -n nextcloud -l app.kubernetes.io/name=onlyoffice --tail=50

# Force re-run the config job (delete it β€” ArgoCD will recreate on next sync)
ssh controller "kubectl delete job onlyoffice-configure -n nextcloud --ignore-not-found"