9 August 2026

An EU office suite on your own platform: Nextcloud + Vaultwarden with SSO

Files and passwords for a small company, self-hosted behind one sign-on — including the security property that looks like a broken feature until you understand it.

nextcloudvaultwardenselfhostedssokuberneteseu-sovereignty


At some point a platform stops being an exercise and starts holding things you would actually miss. For most small companies that moment is files and passwords — the two categories where "we'll migrate it later" quietly becomes "we can't."

So this is where the platform earns its keep: a file-sync-and-share service and a password manager, both running on the same cheap box, both behind the single sign-on from the last post, both backed by the shared Postgres from the one before that. Nothing here is a demo.

What you'll get from this post

Two real applications deployed the platform way — one a large community Helm chart, one a handful of plain manifests — wired into OIDC so there's exactly one login, with the honest account of what SSO does and does not protect in a password manager, and why one of the security properties that reads like a limitation is the reason to trust it.

Two apps, two very different shapes

The interesting thing about deploying these together is that they stress opposite ends of the platform contract.

Vaultwarden is what the contract was designed for: a single container that listens on a port, reads its config from environment variables, and speaks Postgres. It runs as a non-root user with a read-only root filesystem and a liveness endpoint, and the whole deployment is a few plain manifests. Before committing anything I ran the image locally to confirm exactly that — clean start as uid 65532, read-only root, /alive returning 200, and Diesel's migrations executing correctly against a real Postgres.

Nextcloud is the opposite: a large community Helm chart with subcharts, a cron sidecar, a PVC, and configuration that doesn't fit in environment variables. It's the honest test — a platform that only runs your own purpose-built containers isn't a platform, it's a framework.

Nextcloud also came with a supply-chain surprise. The chart's bundled Redis is a Bitnami subchart, and Broadcom deleted the Bitnami catalog. So the dependency is disabled and the platform runs its own small Redis instead. Worth internalising as a general lesson: a chart's dependencies are somebody else's dependencies, and "it's in the official chart" is not a durability guarantee.

The parts a Helm chart can't express

Nextcloud's OIDC setup isn't a values key. It's a sequence of occ commands — install the user_oidc app, register the provider, point it at Keycloak. The temptation is to run them once by hand and move on, which puts the app's actual configuration outside git forever.

Instead they run as an Argo CD PostSync hook, after the chart's resources exist:

metadata:
  annotations:
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation

occ commands are idempotent, so re-running on every sync is safe — and that property is what makes the claim "a fresh sync reproduces the whole configuration" actually true rather than aspirational.

One deliberate non-change: local login is never disabled. Forcing user_oidc as the login form's only backend is right, but it isn't the same as removing the emergency door. The direct-login URL stays reachable, because "sign in with Keycloak" is not a recovery plan for "Keycloak is down" — the same reasoning that keeps Argo CD's local admin account alive.

The feature that looks like a bug

Vaultwarden gained native OIDC SSO upstream in 1.35, which matters more than it sounds: the self-hosted Bitwarden crowd spent years running a community fork to get this. It's now in the mainline project, so the platform runs SIGNUPS_ALLOWED=false and single sign-on becomes the only way a user comes into existence.

Now the part that generates confused forum posts: you still have a master password, and SSO does not replace it.

That reads like a half-finished integration. It isn't. Your vault is encrypted client-side with a key derived from the master password. The server — this server, the one I operate — never sees that key and cannot decrypt your vault. If SSO replaced the master password, then whoever controls the identity provider could silently read every secret in it. That would be me.

So the two things are doing genuinely different jobs. SSO answers may this person reach the service. The master password answers may this person read the data. Collapsing them into one would trade the entire security model for one less prompt.

The operational consequence is that the master password belongs on the offline break-glass list, not in the platform — a distinction that comes up again, at length, in a later post. And it's the practical illustration of the rule this project keeps repeating: Vaultwarden is the password manager for humans, OpenBao is the secret store for machines. Don't blur them.

The network policies that did nothing for a month

Both apps shipped with default-deny NetworkPolicy resources: allow Traefik in, allow Postgres, DNS, Keycloak and SMTP out, deny everything else. Textbook.

They enforced nothing at all.

The cluster's CNI was flannel — the Talos default, never explicitly chosen — and flannel does not implement the NetworkPolicy API. The resources were accepted by the API server, appeared in kubectl get netpol, and restricted no traffic whatsoever. Correct, portable, entirely decorative.

This is the worst class of security bug, because every observable signal says you're fine. Nothing errors. Nothing warns. kubectl describe shows exactly the policy you wrote. The only way to find it is to know that the API being accepted and the API being implemented are different things — and then to actually test that a denied connection is denied.

Fixing it meant replacing the CNI, which sits below the GitOps layer entirely: a node isn't Ready without a CNI, so Argo CD can't be the thing that installs one. Cilium now ships from the infrastructure side, kube-proxy-free, and the policies that were decorative are enforced.

The policies themselves never changed. They were right the whole time — which is precisely why nobody noticed.

What this really buys you

A company can run its files and its passwords on hardware it chose, in a jurisdiction it chose, with one login and no per-seat pricing. That's the visible part.

The invisible part is that both apps arrived through the same path as everything else — declared in git, secrets from the vault, database from the shared cluster, backed up by the same schedule — so neither is a special snowflake somebody has to remember how to rebuild.

Gotchas

Every one of these cost real time.

  • A NetworkPolicy your CNI doesn't implement is silently inert. No error, no warning, correct-looking kubectl output, zero enforcement. Verify with an actual denied connection, not by reading the manifest.
  • Deleting a PostSync hook Job does not re-run it. The hook fires on Application sync, so removing the Job just removes the Job. Trigger a sync instead — an easy hour to lose when a config change appears not to apply.
  • Nextcloud's web pod needs a restart after an app install, or its routes return 404 for the thing you just installed. Perfectly reproducible, deeply confusing the first time.
  • App-store app code lives on the PVC, not in the image. So a storage problem doesn't just cost you file content — it can take out OIDC login itself, because the authentication app is data now. Worth knowing before you reason about blast radius.
  • Generate database passwords alphanumeric-only. One containing a / broke Vaultwarden's URL-based connection string — the Rust URL parser read it as a path separator. The error pointed everywhere except the password. (Same trap as the previous post; it bit twice, in different applications.)
  • A chart's bundled dependency can be deleted from the internet. The Bitnami Redis subchart stopped existing. Run your own, or at minimum know which upstream registries a helm install is quietly trusting.
  • Keep the emergency door. SSO_ONLY stays off until single sign-on is verified end to end on desktop and mobile, and Nextcloud's direct-login URL is never removed. Locking yourself out of the password manager with the password manager is a genuinely bad afternoon.

Where this leaves us

There are now things on this platform that would genuinely hurt to lose: company files, and the vault holding everyone's credentials.

Which makes the next question unavoidable, and it's the one this whole series has been circling. Not "is it backed up" — everything above is backed up. The question is whether the backups restore. Next post: Velero, the table of what each backup does not cover, and a restore day with real numbers.


I'm building this in the open, and I'll run it for EU teams who'd rather ship product than hire a platform engineer. If that's you, follow along via RSS — every build post lands there first — or start with what this project is about.