Every platform team eventually hits the same small, annoying problem: some application on Kubernetes keeps its data on a shared volume — label templates for a print server, custom report definitions, a report database — and a human operator needs to get files in or out of it. Not a developer with kubectl, not a CI pipeline. An operator, from a browser.
The usual answers are all bad. kubectl cp means giving out cluster credentials. A shell into the pod is worse. An SFTP sidecar is a second auth system to run and audit. So I built file-portal — and I’ve just open-sourced it under MIT.
What it is
file-portal is a Helm chart plus a small FastAPI app. You point it at PVCs that already exist in a namespace, and it serves them as a web portal behind SSO:
- ReadOnlyMany (ROX) volumes are export-only — browse and download.
- ReadWriteMany (RWX) volumes get export + import — upload and delete too.
That’s the whole product surface. No database, no accounts, no background jobs.
The one design rule: be boring
The app has almost no logic, on purpose. There’s no dependency injection, no plugin system, no clever abstractions. The only two pieces of real behaviour are a read-only gate and an audit logger. Everything else is FastAPI serving files off a mounted filesystem.
Read-only is the part I care most about, because “the UI hides the upload button” is not a security control. In file-portal, a ROX volume is read-only at three layers: the Kubernetes mount is readOnly: true, the UI never renders an import control, and the handler returns a 403 if you POST to it anyway. The mount is the one that matters — even if the app had a bug, the kernel won’t let it write.
Every action, and every denied action, is written to the audit log by the operator’s email. If someone downloads a report or is refused an upload, there’s a line for it on stdout, ready for your log pipeline.
Authentication is not the app’s job
file-portal does no authentication itself. An oauth2-proxy sidecar sits in front, authenticates against your IDP over OIDC, and forwards identity as headers (X-Forwarded-Email, X-Forwarded-Groups). In production the app fails closed: no X-Forwarded-Email, no request served.
Authorization is two group tiers, enforced at the proxy and the app:
- readers — browse + download.
- writers — also import + delete (a superset of readers).
Who may open the portal at all is readGroups ∪ writeGroups; non-members never reach the app. It works with any OIDC provider — Keycloak, Okta, and Entra ID (Azure AD) each get copy-paste values in the docs, including the fiddly bits like Entra’s App Roles and Okta’s classic sign-out.
The Kubernetes-specific sharp edges
A few things that took real debugging and are baked into the chart and its docs:
- It references PVCs by name; it never creates them. A pod can only mount PVCs in its own namespace, so the portal installs into the namespace the volumes live in.
- ReadWriteOnce is rejected — at
helm installtime and again at app startup. An RWO PVC binds to a single pod, so a separate portal pod can’t mount a volume another workload already holds. If you need shared write, the volume has to be RWX. - NFS write identity. These shares are NFS-backed and owned
root:1001with a group-writable setgid directory, so write permission comes from group 1001, not the user. The chart defaults tofsGroup: 1001so uploads just work — until you hit OpenShift, which assigns a random UID withgid 0and needs group 1001 added as a supplemental group (and sometimes a custom SCC). All of that is documented. - Oversized OIDC cookies. Keycloak tokens with a
groupsclaim get chunked across cookies and blow past the default ~4–8 KB proxy header buffers, producing a cryptic “invalid response from upstream” after a successful login. The fix lives on the ingress/gateway, not the app — so the README explains it for Kong, ingress-nginx, and Envoy-based gateways.
Running it
You build the image from the included Dockerfile, push it to a registry your cluster can pull from, and point the chart at it. Then it’s one helm upgrade --install with a values file. It runs unchanged on vanilla Kubernetes and OpenShift.
Open source
file-portal is MIT licensed — chart, app source, and any image you build from it. It’s deliberately small; if it solves your “operators need files off a PVC” problem, take it, fork it, make it yours.
Repo: https://github.com/infrabytes-io/file-portal
If you’d like access to the code repository, please get in touch.
