| .forgejo/workflows | ||
| cmd/cairnd | ||
| deploy | ||
| docs | ||
| internal | ||
| proto/cairn/peer/v1 | ||
| web | ||
| .gitignore | ||
| .ko.yaml | ||
| ARCHITECTURE.md | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| ENCRYPTION-COMPARISON.md | ||
| EULA.md | ||
| example.config.json | ||
| go.mod | ||
| go.sum | ||
| IMPLEMENTED-PEERING.md | ||
| LICENSE.md | ||
| Makefile | ||
| OCM.md | ||
| OIDC-COMPARISON.md | ||
| PEERING.md | ||
| README.md | ||
| VERSIONING.md | ||
Cairn
Self-hosted file storage. Files, and nothing else.
Cairn is a single-binary file server: a clean web interface, WebDAV for every sync client, resumable uploads, share links, pluggable storage (POSIX/NFS or any S3-compatible object store), and pluggable identity (local, OIDC, or LDAP). It is what remains of a "Nextcloud" after you remove everything that is not file storage.
- One container is the whole stack. No PHP, no Redis, no required database, no cron sidecar. One Go binary, one config file.
- Your storage backend is the source of truth. On the POSIX driver the
directory tree on disk is the data model — browse it with
ls, back it up withrsync, snapshot it with ZFS. On the S3 driver your bucket is the data model. No opaque blobs, no schema lock-in, no database index to corrupt. Even search works this way: a bounded walk over the backend, no index. Since v0.6, instance state — settings, license, share links, legal holds, peering trust — lives with the backend too, encrypted under a reserved prefix: a fresh container against the same bucket is the same instance, and updates cannot lose it. - Identity is consumed, not implemented. Bring your own OIDC provider (Keycloak, Authentik, Authelia, Zitadel, ...) or LDAP directory (Active Directory, OpenLDAP, FreeIPA). Groups flow through both — shared spaces and even admin can be managed entirely in your directory. Local argon2id users work out of the box.
- Small trust surface. The core file server carries five Go dependencies:
x/crypto(argon2id),go-oidc+x/oauth2(OIDC),go-ldap(LDAP), andfilippo.io/age(at-rest encryption) — plus exactly one vendored browser asset, a pinned build of typage (the same author's TypeScript age) used by the share viewer, embedded in the binary rather than loaded from a CDN. Instance peering adds two more,google.golang.org/grpcandgoogle.golang.org/protobuf, and is the one place that discipline was deliberately broken: it is opt-in, off unless a peering is configured, and carries only small control messages — the file bytes still move over stdlib HTTP. The reasoning is recorded in IMPLEMENTED-PEERING.md §7.
Status: v0.6, dogfooded daily on a k3s cluster (S3(Garage) + OIDC via Keycloak + at-rest encryption; client interop verified with Finder, rclone, and Cyberduck; the delete-the-volume recovery drill rehearsed on the live instance). Pre-1.0: minor versions may break config. See ARCHITECTURE.md for the full design, docs/handbook/ for operator and user guides, and CHANGELOG.md for what shipped when. Product site: www.swisscairn.ch.
Features
- Web UI — sidebar views (Personal, group Spaces, Recents, Favourites, Shares, Transfers, Federated, Deleted files), sortable list and tile views with server-generated image thumbnails, per-browser view options (hidden files, extensions, page size, tile size), multi-select with bulk download / copy / move / favourites / delete, per-row actions (open, edit, download, share, copy link, send to peer, copy to, move, rename, create a space from the selection, lock, details), a New menu that creates folders and documents, search across your tree, a notification bell (received and sent transfers, expired locks — history retention-bounded), chevron breadcrumbs, branded login page, dark mode + accent themes, full UI in English, German, French, and Italian. Embedded in the binary; no separate frontend to deploy.
- WebDAV at
/dav/— mount as a network drive from Finder, Windows Explorer, rclone, Cyberduck. RFC 4331 quota properties so clients show free space, and real class-2LOCK/UNLOCKso Office and Finder hold genuine locks while editing. Two-way desktop sync via rclone bisync: see docs/SYNC.md. - Resumable uploads (TUS 1.0.0) at
/api/v1/tus/— interrupted uploads survive dropped connections and server restarts; the web UI uses this automatically for files ≥16 MiB, resuming across page reloads. - Share links — optional public
/s/surface with expiry, password, and upload-drop support; admins can list and revoke all links. - Sharing with people and groups — address a file or folder to another account or a group on this instance: recipients sign in as themselves and find it under Shares › Shared with me, reading the owner's file in place. Roles are view or edit; only the owner manages the grant list (recipients cannot re-share), and internal sharing has its own admin switch, separate from public links.
- In-browser document editing (WOPI) — point
office.urlat a separately deployed Collabora Online (or any WOPI-compatible editor) and office documents open in a full-page editor: editable formats come from the editor's own discovery, Microsoft and ODF files are edited natively, the New menu creates documents from templates embedded in the binary, and locked / held / read-only files open honestly read-only with the reason named. Cairn ships none of the editor's code. See docs/handbook/document-editing.md. - Instance peering (federation v1) — two Cairn instances whose admins
explicitly paired them can send files to each other's users: per-peer
Ed25519 keys exchanged out of band (shown once; only a verifier is
stored), separate send/receive allow-lists of users and IdP groups per
peer, and delivery into the recipient's
Personal/Inbox/<peer>/. Files are age-encrypted end to end for the receiving instance before they leave the sender, so a TLS-terminating proxy at either end sees ciphertext only. No discovery, no central broker, no identity federation — an explicit admin handshake or nothing. Runtime on/off switch, per-peer connection test, transfer notifications in the UI. Requiresauth.perUserHomes(the default). See docs/handbook/peering.md. - Open Cloud Mesh (OCM) — federate with Nextcloud, ownCloud, OpenCloud, Seafile and CERNBox: standards-track OCM speaking both deployed dialects (modern 1.1 and the legacy Nextcloud/ownCloud form), RFC 9421 and draft-cavage request signatures, and live access grants instead of copies. Off by default, behind a server allow-list that actually gates both directions, per-server strictness ("invited contacts only" by default) and user-owned invites. Received shares live in a dedicated Federated view — never mounted into sync trees, never copied silently. See docs/handbook/federation-ocm.md.
- Storage drivers —
posix(any mounted filesystem) ors3(hand-rolled SigV4 client, works against Garage/MinIO/AWS). An instance can also be deployed withstorage.driver: "setup"and connected to its storage from the browser: a token-gated wizard probes the target with a real write, persists a local bootstrap pointer, and restarts into the normal boot path. - Auth modes —
local(config-file users with argon2id hashes, up to 50 runtime accounts managed from the dashboard, and a first-run wizard that mints the initial admin behind a one-time token from the logs),oidc(Authorization Code + PKCE against any OIDC IdP — local accounts and app passwords work alongside), orldap(search-then-bind against AD / OpenLDAP / FreeIPA). See docs/handbook/local-accounts.md. - Authorization (
auth.perUserHomes, on by default) — each user gets a private/home, plus shared/spaces/<group>mapped from OIDC group claims or LDAPmemberOf; admins see the whole backend. Set it tofalseonly if you deliberately want a single shared namespace: the Personal/Spaces split users expect is not truthful without it, because "Personal" would be the shared backend root. Spaces are self-service: users create a space from the UI and the owner manages members (read or read+write) — app-owned by default (membership lives with the encrypted instance state; no IdP write access needed), with an opt-in power mode that brokers spaces to the directory instead (ADR 0001). With the IdP read client configured, directory-group membership changes apply live (~1 minute, revocations included) instead of at next login. Spaces have a full lifecycle: deactivate (reversible, hidden from members) and then delete — contents go to trash for the retention window and the name stays reserved until final purge. Admin itself can be granted by directory group (auth.adminGroups) — withauth.adminsas the break-glass username list. - Accounts & groups — the Users & access page manages local accounts and app-owned groups that work everywhere directory groups do (peering allow-lists, the admin group) — one namespace with the directory, collisions refused. Directory accounts and groups stay read-only: identity is consumed, not implemented. See docs/handbook/groups.md.
- Trash (opt-in
storage.trash.enabled) — deletes move to a per-user trash with restore/purge in the UI and API; deletions inside a shared space land in the space's own trash, visible to all members, restorable by write members. Admins govern the whole lifecycle from the Deleted files admin topic: the retention window, a deliberate never-delete-automatically mode (the default), instance-wide manual purge, and a purge log of every final deletion. File versioning is deliberately delegated to backend snapshots (ZFS, S3 versioning). - Data governance — append-only audit log (
GET /api/v1/admin/audit), legal holds that block delete/overwrite/rename for everyone (admins included), path retention windows, a trash lifecycle (retention sweep and admin manual purge, both behind retention + hold guard rails, with a purge log), read-only spaces (a-rodirectory group is mounted view-only), and per-file locks — user- or Office-held, folder locks for space owners, an optional instance-wide expiry cap, and a live admin Locks inventory with audited break-lock — plus optional download marking for leak tracing: authenticated downloads of PDF/Office/image files carry an invisible pseudonymous mark, so a leaked copy can be traced to the exact download it came from (evidence, not a verdict — and off by default, with the per-country legal homework shipped in the handbook). Admin-driven, with UI. - Quota — reporting (
GET /api/v1/quota, WebDAV RFC 4331) plus enforcedmaxFileBytes/maxTotalBytes/reserveBytes/ per-userperUserByteslimits → 507 on breach. - At-rest encryption — optional age-based content encryption with
per-user and per-space keys held in the Keycloak profile, an OpenBao
KV mount (uniform custody for AD deployments), or a single deployment key
with per-instance domain separation. Always encrypts to a recovery
recipient so key-loss ≠ data-loss (
age -dworks offline) — and the same offline drill opens the encrypted instance-state objects, so your instance's identity is as recoverable as its files. Optional post-quantum mode: hybrid ML-KEM-768+X25519 identities with automatic upgrade-on-touch migration. See docs/handbook/encryption.md and ENCRYPTION-COMPARISON.md. - Fair-code licensing, built to never hurt you — free for up to 50 users with no license key — and no key installed means no phone-home, ever. With a licence, above or below 50 users: Ed25519-signed keys verified fully offline, a monthly check-in carrying the licence id and nothing else, a 7-day grace window, and enforcement that only ever blocks new sign-ins over the cap — existing users and file access are never touched. Admin panel: paste-to-install, and one-click Remove licence for an instant, harmless return to the free tier.
- Ops — Prometheus
/metrics,/healthz, graceful shutdown, admin API (/api/v1/admin/*), structured logs. Multi-arch images (amd64/arm64) built by CI via ko; Helm chart atdeploy/chart.
Quick start
make build
cp example.config.json config.json
echo -n 'yourpassword' | ./bin/cairnd hash-password # paste into config.json
./bin/cairnd -config config.json
Open http://localhost:7420 — or mount http://localhost:7420/dav/ as a
network drive with any WebDAV client.
Docker:
docker compose -f deploy/docker-compose.example.yml up -d
Kubernetes (Helm):
helm install cairn deploy/chart -f your-values.yaml
Configuration
Minimal (local auth, POSIX storage):
{
"listen": ":7420",
"storage": { "driver": "posix", "root": "/data" },
"auth": {
"mode": "local",
"users": [{ "username": "admin", "passwordHash": "<cairnd hash-password output>" }]
},
"enableWebdav": true
}
Everything else is opt-in blocks on top of this: auth.mode: "oidc" or
"ldap" with their settings, storage.driver: "s3" (or "setup"), shares,
storage.quota, storage.trash, storage.encryption, auth.admins,
auth.adminGroups, auth.perUserHomes, auth.oidcLogoutUrl, license,
peering, preview, office. See
example.config.json and the field comments in
internal/config/config.go — the config structs are the reference
documentation. Operator guides live in docs/handbook/.
Secrets can come from env instead of the file: CAIRN_S3_SECRET_KEY,
CAIRN_OIDC_CLIENT_SECRET, CAIRN_LDAP_BIND_PASSWORD,
CAIRN_ENC_RECOVERY_IDENTITY, CAIRN_LICENSE_KEY, and friends (env wins).
Non-goals
Deliberately out of scope, so Cairn stays one auditable binary: calendars, contacts, office suites (in-browser editing plugs in via WOPI from a separately deployed editor — Cairn ships none of its code), app stores and plugins, per-file ACLs, a metadata database, a search index, and app-layer file versioning (that's your filesystem's or object store's job). See ARCHITECTURE.md §2.
License
Fair-code / source-available: free for personal use and organizations up to 50 users; commercial license required above that or to offer Cairn as a hosted service — purchased at www.swisscairn.ch. See LICENSE.md. Cairn is not OSI open source and does not claim to be. Use of the software — both tiers — is additionally governed by the EULA: what data reaches the vendor (nothing without a licence key; the licence id only, with one), warranty, liability, and the operator's responsibility for lawful use of the optional monitoring features.